1#![forbid(unsafe_code)]
16#![warn(missing_docs)]
17
18pub const GEO_LAT_MIN: f64 = -85.05112878;
22pub const GEO_LAT_MAX: f64 = 85.05112878;
24pub const GEO_LON_MIN: f64 = -180.0;
26pub const GEO_LON_MAX: f64 = 180.0;
28pub const EARTH_RADIUS_METERS: f64 = 6_372_797.560_856;
31pub const GEO_STEP: u32 = 26;
33
34pub fn encode_score(lon: f64, lat: f64) -> Option<f64> {
42 if !(lon.is_finite() && lat.is_finite()) {
43 return None;
44 }
45 if !(GEO_LAT_MIN..=GEO_LAT_MAX).contains(&lat) {
46 return None;
47 }
48 if !(GEO_LON_MIN..=GEO_LON_MAX).contains(&lon) {
49 return None;
50 }
51 let bits = encode_bits_wgs84(lon, lat);
52 Some(bits as f64)
53}
54
55pub fn decode_score(score: f64) -> (f64, f64) {
59 let bits = score_to_bits(score);
60 let (ilat, ilon) = deinterleave52(bits);
61 cell_centre(ilon, ilat, GEO_LON_MIN, GEO_LON_MAX, GEO_LAT_MIN, GEO_LAT_MAX)
62}
63
64pub fn haversine_meters(lon1: f64, lat1: f64, lon2: f64, lat2: f64) -> f64 {
68 let phi1 = lat1.to_radians();
69 let phi2 = lat2.to_radians();
70 let dphi = (lat2 - lat1).to_radians();
71 let dlam = (lon2 - lon1).to_radians();
72 let a = (dphi * 0.5).sin().powi(2)
73 + phi1.cos() * phi2.cos() * (dlam * 0.5).sin().powi(2);
74 let c = 2.0 * a.sqrt().clamp(0.0, 1.0).asin();
75 EARTH_RADIUS_METERS * c
76}
77
78pub fn encode_base32_geohash(lon: f64, lat: f64) -> [u8; 11] {
84 const ALPHABET: &[u8; 32] = b"0123456789bcdefghjkmnpqrstuvwxyz";
85 let bits = encode_bits_full_range(lon, lat);
86 let mut out = [0u8; 11];
87 for (i, slot) in out.iter_mut().enumerate() {
88 let shift = 52i32 - (i as i32 + 1) * 5;
89 let idx = if shift >= 0 {
93 ((bits >> shift) & 0x1f) as usize
94 } else {
95 0
96 };
97 *slot = ALPHABET[idx];
98 }
99 out
100}
101
102fn interleave52(lat: u32, lon: u32) -> u64 {
106 spread26(lat as u64) | (spread26(lon as u64) << 1)
107}
108
109fn deinterleave52(bits: u64) -> (u32, u32) {
112 let lat = pack26(bits) as u32;
113 let lon = pack26(bits >> 1) as u32;
114 (lat, lon)
115}
116
117fn spread26(mut x: u64) -> u64 {
120 x &= 0x3ff_ffff; x = (x | (x << 16)) & 0x0000_0000_FFFF_0000_FFFF;
122 x = (x | (x << 8)) & 0x0000_00FF_00FF_00FF_00FF;
123 x = (x | (x << 4)) & 0x000F_0F0F_0F0F_0F0F;
124 x = (x | (x << 2)) & 0x3333_3333_3333_3333;
125 x = (x | (x << 1)) & 0x5555_5555_5555_5555;
126 x
127}
128
129fn pack26(mut x: u64) -> u64 {
132 x &= 0x5555_5555_5555_5555;
133 x = (x | (x >> 1)) & 0x3333_3333_3333_3333;
134 x = (x | (x >> 2)) & 0x000F_0F0F_0F0F_0F0F;
135 x = (x | (x >> 4)) & 0x0000_00FF_00FF_00FF_00FF;
136 x = (x | (x >> 8)) & 0x0000_0000_FFFF_0000_FFFF;
137 x = (x | (x >> 16)) & 0x3ff_ffff;
138 x
139}
140
141fn encode_bits_wgs84(lon: f64, lat: f64) -> u64 {
142 encode_bits(lon, lat, GEO_LON_MIN, GEO_LON_MAX, GEO_LAT_MIN, GEO_LAT_MAX)
143}
144
145fn encode_bits_full_range(lon: f64, lat: f64) -> u64 {
146 encode_bits(lon, lat, GEO_LON_MIN, GEO_LON_MAX, -90.0, 90.0)
147}
148
149fn encode_bits(
150 lon: f64,
151 lat: f64,
152 lon_min: f64,
153 lon_max: f64,
154 lat_min: f64,
155 lat_max: f64,
156) -> u64 {
157 let cells = (1u64 << GEO_STEP) as f64;
158 let lat_off = ((lat - lat_min) / (lat_max - lat_min)) * cells;
159 let lon_off = ((lon - lon_min) / (lon_max - lon_min)) * cells;
160 let lat_u = (lat_off as u32).min((1 << GEO_STEP) - 1);
161 let lon_u = (lon_off as u32).min((1 << GEO_STEP) - 1);
162 interleave52(lat_u, lon_u)
163}
164
165fn cell_centre(
166 ilon: u32,
167 ilat: u32,
168 lon_min: f64,
169 lon_max: f64,
170 lat_min: f64,
171 lat_max: f64,
172) -> (f64, f64) {
173 let cells = (1u64 << GEO_STEP) as f64;
179 let lon_span = lon_max - lon_min;
180 let lat_span = lat_max - lat_min;
181 let lon_lo = lon_min + (ilon as f64 / cells) * lon_span;
182 let lon_hi = lon_min + ((ilon as f64 + 1.0) / cells) * lon_span;
183 let lat_lo = lat_min + (ilat as f64 / cells) * lat_span;
184 let lat_hi = lat_min + ((ilat as f64 + 1.0) / cells) * lat_span;
185 ((lon_lo + lon_hi) / 2.0, (lat_lo + lat_hi) / 2.0)
186}
187
188fn score_to_bits(score: f64) -> u64 {
193 if !score.is_finite() || score < 0.0 {
194 return 0;
195 }
196 let max = (1u64 << (GEO_STEP * 2)) - 1;
197 let n = score as u64;
198 n.min(max)
199}
200
201pub fn neighbor_score_ranges(lon: f64, lat: f64, radius_m: f64) -> Vec<(f64, f64)> {
216 if !lon.is_finite() || !lat.is_finite() || radius_m <= 0.0 {
217 return vec![(0.0, (1u64 << 52) as f64 - 1.0)];
218 }
219 let step = estimate_step(radius_m);
220 if step <= 1 {
221 return vec![(0.0, (1u64 << 52) as f64 - 1.0)];
222 }
223 let (clat, clon) = encode_uniform_step(lon, lat, step);
224 let mut raw: Vec<(u64, u64)> = Vec::with_capacity(9);
225 let cells = 1i32 << step;
226 let shift = (GEO_STEP - step) * 2;
227 let inner_mask = (1u64 << shift) - 1;
228 for dlat in -1i32..=1 {
229 for dlon in -1i32..=1 {
230 let ilat = clat as i32 + dlat;
231 if !(0..cells).contains(&ilat) {
232 continue;
233 }
234 let ilon = (clon as i32 + dlon).rem_euclid(cells);
235 let prefix = interleave52(ilat as u32, ilon as u32);
236 let min = prefix << shift;
237 let max = min | inner_mask;
238 raw.push((min, max));
239 }
240 }
241 raw.sort_unstable();
242 merge_ranges(raw)
243}
244
245fn estimate_step(radius_m: f64) -> u32 {
246 const MERCATOR_MAX: f64 = 20_037_726.37;
247 if radius_m <= 0.0 {
248 return GEO_STEP;
249 }
250 let mut step = 1u32;
251 let mut r = radius_m;
252 while r < MERCATOR_MAX {
253 r *= 2.0;
254 step += 1;
255 }
256 step.saturating_sub(2).clamp(1, GEO_STEP)
257}
258
259fn encode_uniform_step(lon: f64, lat: f64, step: u32) -> (u32, u32) {
260 let cells = (1u64 << step) as f64;
261 let lat_clamped = lat.clamp(GEO_LAT_MIN, GEO_LAT_MAX);
262 let lon_clamped = lon.clamp(GEO_LON_MIN, GEO_LON_MAX);
263 let lat_off =
264 ((lat_clamped - GEO_LAT_MIN) / (GEO_LAT_MAX - GEO_LAT_MIN) * cells) as u32;
265 let lon_off =
266 ((lon_clamped - GEO_LON_MIN) / (GEO_LON_MAX - GEO_LON_MIN) * cells) as u32;
267 let max = (1u32 << step) - 1;
268 (lat_off.min(max), lon_off.min(max))
269}
270
271fn merge_ranges(sorted: Vec<(u64, u64)>) -> Vec<(f64, f64)> {
275 let mut out: Vec<(u64, u64)> = Vec::with_capacity(sorted.len());
276 for (min, max) in sorted {
277 match out.last_mut() {
278 Some(prev) if prev.1.saturating_add(1) >= min => {
279 prev.1 = prev.1.max(max);
280 }
281 _ => out.push((min, max)),
282 }
283 }
284 out.into_iter().map(|(a, b)| (a as f64, b as f64)).collect()
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 const PALERMO: (f64, f64) = (13.361389, 38.115556);
296 const CATANIA: (f64, f64) = (15.087269, 37.502669);
297
298 #[test]
299 fn geohash_string_matches_redis() {
300 assert_eq!(&encode_base32_geohash(PALERMO.0, PALERMO.1), b"sqc8b49rny0");
302 assert_eq!(&encode_base32_geohash(CATANIA.0, CATANIA.1), b"sqdtr74hyu0");
303 }
304
305 #[test]
306 fn geopos_roundtrip_matches_redis_to_the_last_digit() {
307 for (lon, lat, want_lon, want_lat) in [
310 (
311 PALERMO.0,
312 PALERMO.1,
313 "13.36138933897018433",
314 "38.11555639549629859",
315 ),
316 (
317 CATANIA.0,
318 CATANIA.1,
319 "15.08726745843887329",
320 "37.50266842333162032",
321 ),
322 ] {
323 let score = encode_score(lon, lat).expect("in range");
324 let (dlon, dlat) = decode_score(score);
325 assert_eq!(format!("{dlon:.17}"), want_lon, "lon for ({lon},{lat})");
326 assert_eq!(format!("{dlat:.17}"), want_lat, "lat for ({lon},{lat})");
327 }
328 }
329}