1use std::path::Path;
4
5use rusqlite::{params, Connection, OpenFlags, Result as SqliteResult};
6
7#[derive(Debug, Clone)]
9pub struct GeocodingResult {
10 pub city: String,
11 pub country: String,
12}
13
14pub struct GeocodingService {
16 conn: Connection,
17}
18
19impl GeocodingService {
20 pub fn new<P: AsRef<Path>>(db_path: P) -> SqliteResult<Self> {
21 let conn = Connection::open_with_flags(db_path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
22 conn.execute_batch(
23 r#"
24 PRAGMA query_only = ON;
25 PRAGMA cache_size = -10000;
26 PRAGMA mmap_size = 268435456;
27 "#,
28 )?;
29 Ok(Self { conn })
30 }
31
32 const MAX_CITY_DISTANCE_KM: f64 = 100.0;
39
40 pub fn reverse_geocode(&self, lat: f64, lon: f64) -> Option<GeocodingResult> {
41 if !Self::is_valid_coordinate(lat, lon) {
42 return None;
43 }
44
45 self.search_bounding_box(lat, lon, 1.0)
50 .or_else(|| self.search_bounding_box(lat, lon, 2.0))
51 }
52
53 fn feature_priority(code: &str) -> u8 {
62 match code {
63 "PPLC" => 0, "PPLA" => 1, "PPLA2" => 2, "PPLA3" => 3,
67 "PPLA4" => 4,
68 "PPLG" => 5, "PPL" => 6, _ => 7, }
72 }
73
74 fn search_bounding_box(&self, lat: f64, lon: f64, radius_deg: f64) -> Option<GeocodingResult> {
75 let min_lat = (lat - radius_deg).max(-90.0);
76 let max_lat = (lat + radius_deg).min(90.0);
77 let lon_ranges = longitude_ranges(lon, radius_deg);
78
79 let mut cities: Vec<(String, String, String, f64, f64, String)> = Vec::new();
87 for (min_lon, max_lon) in lon_ranges {
88 let mut stmt = self
89 .conn
90 .prepare(
91 r#"
92 SELECT
93 ascii_name,
94 country_name,
95 country_code,
96 latitude,
97 longitude,
98 COALESCE(feature_code, '')
99 FROM cities
100 WHERE latitude BETWEEN ?1 AND ?2
101 AND longitude BETWEEN ?3 AND ?4
102 AND population >= 100000
103 LIMIT 200
104 "#,
105 )
106 .ok()?;
107
108 let mut rows = stmt
109 .query_map(params![min_lat, max_lat, min_lon, max_lon], |row| {
110 Ok((
111 row.get(0)?,
112 row.get(1)?,
113 row.get(2)?,
114 row.get(3)?,
115 row.get(4)?,
116 row.get(5)?,
117 ))
118 })
119 .ok()?
120 .collect::<rusqlite::Result<Vec<_>>>()
121 .ok()?;
122 cities.append(&mut rows);
123 }
124
125 let mut best: Option<(GeocodingResult, u8, f64)> = None;
129
130 for (city_name, country_name, _country_code, city_lat, city_lon, feature_code) in cities {
131 let distance = Self::haversine_distance(lat, lon, city_lat, city_lon);
132 if distance > Self::MAX_CITY_DISTANCE_KM {
133 continue;
134 }
135 let priority = Self::feature_priority(&feature_code);
136 let candidate = GeocodingResult {
137 city: city_name,
138 country: country_name,
139 };
140 let take = match &best {
141 None => true,
142 Some((_, p, d)) => (priority, distance) < (*p, *d),
143 };
144 if take {
145 best = Some((candidate, priority, distance));
146 }
147 }
148
149 best.map(|(r, _, _)| r)
150 }
151
152 fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
153 const EARTH_RADIUS_KM: f64 = 6371.0;
154
155 let lat1_rad = lat1.to_radians();
156 let lat2_rad = lat2.to_radians();
157 let delta_lat = (lat2 - lat1).to_radians();
158 let delta_lon = (lon2 - lon1).to_radians();
159
160 let a = (delta_lat / 2.0).sin().powi(2)
161 + lat1_rad.cos() * lat2_rad.cos() * (delta_lon / 2.0).sin().powi(2);
162 let c = 2.0 * a.sqrt().asin();
163 EARTH_RADIUS_KM * c
164 }
165
166 fn is_valid_coordinate(lat: f64, lon: f64) -> bool {
167 if !((-90.0..=90.0).contains(&lat) && (-180.0..=180.0).contains(&lon)) {
168 return false;
169 }
170 !((lat.abs() < 0.001) && (lon.abs() < 0.001))
171 }
172}
173
174fn longitude_ranges(lon: f64, radius_deg: f64) -> Vec<(f64, f64)> {
175 let min_lon = lon - radius_deg;
176 let max_lon = lon + radius_deg;
177 if min_lon < -180.0 {
178 vec![(min_lon + 360.0, 180.0), (-180.0, max_lon)]
179 } else if max_lon > 180.0 {
180 vec![(min_lon, 180.0), (-180.0, max_lon - 360.0)]
181 } else {
182 vec![(min_lon, max_lon)]
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn test_haversine_distance() {
192 let distance = GeocodingService::haversine_distance(35.6762, 139.6503, 40.7128, -74.0060);
193 assert!((distance - 10850.0).abs() < 200.0);
194 }
195
196 #[test]
197 fn test_valid_coordinates() {
198 assert!(GeocodingService::is_valid_coordinate(35.6762, 139.6503));
199 assert!(!GeocodingService::is_valid_coordinate(0.0, 0.0));
200 assert!(!GeocodingService::is_valid_coordinate(91.0, 0.0));
201 }
202}