commit 4b5d56777a4909500ef4a1b9214ce4ac09c69008
parent c184c0c247305b56f353cdc30841d926125d6bae
Author: triesap <tyson@radroots.org>
Date: Sun, 22 Mar 2026 01:21:59 +0000
core: add location resolver boundary
- add shared location resolver models for reverse lookup country list and country center queries
- expose the location resolver contract on the app backend without coupling any existing feature flow to geocoder usage
- keep the boundary geocoder-neutral by excluding raw country row dumps from the app-facing api
- add core tests for the default reverse options and stable resolver error codes
Diffstat:
2 files changed, 137 insertions(+), 0 deletions(-)
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
@@ -4,10 +4,15 @@ use eframe::egui;
use std::time::Duration;
use zeroize::Zeroizing;
+mod location_resolver;
mod offline_geocoder;
pub const APP_NAME: &str = "Rad Roots";
+pub use location_resolver::{
+ RadrootsLocationCountry, RadrootsLocationPoint, RadrootsLocationResolverError,
+ RadrootsLocationReverseOptions, RadrootsResolvedLocation,
+};
pub use offline_geocoder::{
RadrootsOfflineGeocoderDiagnostic, RadrootsOfflineGeocoderPlatform,
RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind,
@@ -101,6 +106,24 @@ pub trait RadrootsAppBackend {
fn poll_identity_state(&self) -> Result<Option<IdentityGateState>, String> {
Ok(None)
}
+ fn reverse_location(
+ &self,
+ _point: RadrootsLocationPoint,
+ _options: Option<RadrootsLocationReverseOptions>,
+ ) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> {
+ Err(RadrootsLocationResolverError::Unsupported)
+ }
+ fn list_location_countries(
+ &self,
+ ) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> {
+ Err(RadrootsLocationResolverError::Unsupported)
+ }
+ fn location_country_center(
+ &self,
+ _country_id: &str,
+ ) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> {
+ Err(RadrootsLocationResolverError::Unsupported)
+ }
}
#[derive(Debug, Clone, PartialEq, Eq)]
diff --git a/crates/core/src/location_resolver.rs b/crates/core/src/location_resolver.rs
@@ -0,0 +1,114 @@
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct RadrootsLocationPoint {
+ pub lat: f64,
+ pub lng: f64,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct RadrootsLocationReverseOptions {
+ pub limit: usize,
+ pub degree_offset: f64,
+}
+
+impl Default for RadrootsLocationReverseOptions {
+ fn default() -> Self {
+ Self {
+ limit: 1,
+ degree_offset: 0.5,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct RadrootsResolvedLocation {
+ pub id: i64,
+ pub name: String,
+ pub admin1_id: Option<i64>,
+ pub admin1_name: Option<String>,
+ pub country_id: String,
+ pub country_name: Option<String>,
+ pub point: RadrootsLocationPoint,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct RadrootsLocationCountry {
+ pub country_id: String,
+ pub country_name: Option<String>,
+ pub center: RadrootsLocationPoint,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum RadrootsLocationResolverError {
+ Unsupported,
+ Initializing,
+ Unavailable,
+ CountryCenterNotFound { country_id: String },
+ QueryFailed { message: String },
+}
+
+impl RadrootsLocationResolverError {
+ pub fn code(&self) -> &'static str {
+ match self {
+ Self::Unsupported => "unsupported",
+ Self::Initializing => "initializing",
+ Self::Unavailable => "unavailable",
+ Self::CountryCenterNotFound { .. } => "country_center_not_found",
+ Self::QueryFailed { .. } => "query_failed",
+ }
+ }
+
+ pub fn user_message(&self) -> &'static str {
+ match self {
+ Self::Unsupported => "Offline location resolution is not available on this platform.",
+ Self::Initializing => {
+ "Offline location resolution is still initializing on this device."
+ }
+ Self::Unavailable => "Offline location resolution is not available on this device.",
+ Self::CountryCenterNotFound { .. } => "The requested country center is not available.",
+ Self::QueryFailed { .. } => "The offline location query could not be completed.",
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn reverse_options_default_matches_geocoder_defaults() {
+ let options = RadrootsLocationReverseOptions::default();
+
+ assert_eq!(options.limit, 1);
+ assert_eq!(options.degree_offset, 0.5);
+ }
+
+ #[test]
+ fn location_resolver_error_codes_are_stable() {
+ assert_eq!(
+ RadrootsLocationResolverError::Unsupported.code(),
+ "unsupported"
+ );
+ assert_eq!(
+ RadrootsLocationResolverError::Initializing.code(),
+ "initializing"
+ );
+ assert_eq!(
+ RadrootsLocationResolverError::Unavailable.code(),
+ "unavailable"
+ );
+ assert_eq!(
+ RadrootsLocationResolverError::CountryCenterNotFound {
+ country_id: "US".to_owned(),
+ }
+ .code(),
+ "country_center_not_found"
+ );
+ assert_eq!(
+ RadrootsLocationResolverError::QueryFailed {
+ message: "sqlite failed".to_owned(),
+ }
+ .code(),
+ "query_failed"
+ );
+ }
+}