#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub const GEO_LAT_MIN: f64 = -85.05112878;
pub const GEO_LAT_MAX: f64 = 85.05112878;
pub const GEO_LON_MIN: f64 = -180.0;
pub const GEO_LON_MAX: f64 = 180.0;
pub const EARTH_RADIUS_METERS: f64 = 6_372_797.560_856;
pub const GEO_STEP: u32 = 26;
pub fn encode_score(lon: f64, lat: f64) -> Option<f64> {
if !(lon.is_finite() && lat.is_finite()) {
return None;
}
if !(GEO_LAT_MIN..=GEO_LAT_MAX).contains(&lat) {
return None;
}
if !(GEO_LON_MIN..=GEO_LON_MAX).contains(&lon) {
return None;
}
let bits = encode_bits_wgs84(lon, lat);
Some(bits as f64)
}
pub fn decode_score(score: f64) -> (f64, f64) {
let bits = score_to_bits(score);
let (ilat, ilon) = deinterleave52(bits);
cell_centre(ilon, ilat, GEO_LON_MIN, GEO_LON_MAX, GEO_LAT_MIN, GEO_LAT_MAX)
}
pub fn haversine_meters(lon1: f64, lat1: f64, lon2: f64, lat2: f64) -> f64 {
let phi1 = lat1.to_radians();
let phi2 = lat2.to_radians();
let dphi = (lat2 - lat1).to_radians();
let dlam = (lon2 - lon1).to_radians();
let a = (dphi * 0.5).sin().powi(2)
+ phi1.cos() * phi2.cos() * (dlam * 0.5).sin().powi(2);
let c = 2.0 * a.sqrt().clamp(0.0, 1.0).asin();
EARTH_RADIUS_METERS * c
}
pub fn encode_base32_geohash(lon: f64, lat: f64) -> [u8; 11] {
const ALPHABET: &[u8; 32] = b"0123456789bcdefghjkmnpqrstuvwxyz";
let bits = encode_bits_full_range(lon, lat);
let mut out = [0u8; 11];
for (i, slot) in out.iter_mut().enumerate() {
let shift = 52i32 - (i as i32 + 1) * 5;
let idx = if shift >= 0 {
((bits >> shift) & 0x1f) as usize
} else {
0
};
*slot = ALPHABET[idx];
}
out
}
fn interleave52(lat: u32, lon: u32) -> u64 {
spread26(lat as u64) | (spread26(lon as u64) << 1)
}
fn deinterleave52(bits: u64) -> (u32, u32) {
let lat = pack26(bits) as u32;
let lon = pack26(bits >> 1) as u32;
(lat, lon)
}
fn spread26(mut x: u64) -> u64 {
x &= 0x3ff_ffff; x = (x | (x << 16)) & 0x0000_0000_FFFF_0000_FFFF;
x = (x | (x << 8)) & 0x0000_00FF_00FF_00FF_00FF;
x = (x | (x << 4)) & 0x000F_0F0F_0F0F_0F0F;
x = (x | (x << 2)) & 0x3333_3333_3333_3333;
x = (x | (x << 1)) & 0x5555_5555_5555_5555;
x
}
fn pack26(mut x: u64) -> u64 {
x &= 0x5555_5555_5555_5555;
x = (x | (x >> 1)) & 0x3333_3333_3333_3333;
x = (x | (x >> 2)) & 0x000F_0F0F_0F0F_0F0F;
x = (x | (x >> 4)) & 0x0000_00FF_00FF_00FF_00FF;
x = (x | (x >> 8)) & 0x0000_0000_FFFF_0000_FFFF;
x = (x | (x >> 16)) & 0x3ff_ffff;
x
}
fn encode_bits_wgs84(lon: f64, lat: f64) -> u64 {
encode_bits(lon, lat, GEO_LON_MIN, GEO_LON_MAX, GEO_LAT_MIN, GEO_LAT_MAX)
}
fn encode_bits_full_range(lon: f64, lat: f64) -> u64 {
encode_bits(lon, lat, GEO_LON_MIN, GEO_LON_MAX, -90.0, 90.0)
}
fn encode_bits(
lon: f64,
lat: f64,
lon_min: f64,
lon_max: f64,
lat_min: f64,
lat_max: f64,
) -> u64 {
let cells = (1u64 << GEO_STEP) as f64;
let lat_off = ((lat - lat_min) / (lat_max - lat_min)) * cells;
let lon_off = ((lon - lon_min) / (lon_max - lon_min)) * cells;
let lat_u = (lat_off as u32).min((1 << GEO_STEP) - 1);
let lon_u = (lon_off as u32).min((1 << GEO_STEP) - 1);
interleave52(lat_u, lon_u)
}
fn cell_centre(
ilon: u32,
ilat: u32,
lon_min: f64,
lon_max: f64,
lat_min: f64,
lat_max: f64,
) -> (f64, f64) {
let cells = (1u64 << GEO_STEP) as f64;
let lon_span = lon_max - lon_min;
let lat_span = lat_max - lat_min;
let lon_lo = lon_min + (ilon as f64 / cells) * lon_span;
let lon_hi = lon_min + ((ilon as f64 + 1.0) / cells) * lon_span;
let lat_lo = lat_min + (ilat as f64 / cells) * lat_span;
let lat_hi = lat_min + ((ilat as f64 + 1.0) / cells) * lat_span;
((lon_lo + lon_hi) / 2.0, (lat_lo + lat_hi) / 2.0)
}
fn score_to_bits(score: f64) -> u64 {
if !score.is_finite() || score < 0.0 {
return 0;
}
let max = (1u64 << (GEO_STEP * 2)) - 1;
let n = score as u64;
n.min(max)
}
pub fn neighbor_score_ranges(lon: f64, lat: f64, radius_m: f64) -> Vec<(f64, f64)> {
if !lon.is_finite() || !lat.is_finite() || radius_m <= 0.0 {
return vec![(0.0, (1u64 << 52) as f64 - 1.0)];
}
let step = estimate_step(radius_m);
if step <= 1 {
return vec![(0.0, (1u64 << 52) as f64 - 1.0)];
}
let (clat, clon) = encode_uniform_step(lon, lat, step);
let mut raw: Vec<(u64, u64)> = Vec::with_capacity(9);
let cells = 1i32 << step;
let shift = (GEO_STEP - step) * 2;
let inner_mask = (1u64 << shift) - 1;
for dlat in -1i32..=1 {
for dlon in -1i32..=1 {
let ilat = clat as i32 + dlat;
if !(0..cells).contains(&ilat) {
continue;
}
let ilon = (clon as i32 + dlon).rem_euclid(cells);
let prefix = interleave52(ilat as u32, ilon as u32);
let min = prefix << shift;
let max = min | inner_mask;
raw.push((min, max));
}
}
raw.sort_unstable();
merge_ranges(raw)
}
fn estimate_step(radius_m: f64) -> u32 {
const MERCATOR_MAX: f64 = 20_037_726.37;
if radius_m <= 0.0 {
return GEO_STEP;
}
let mut step = 1u32;
let mut r = radius_m;
while r < MERCATOR_MAX {
r *= 2.0;
step += 1;
}
step.saturating_sub(2).clamp(1, GEO_STEP)
}
fn encode_uniform_step(lon: f64, lat: f64, step: u32) -> (u32, u32) {
let cells = (1u64 << step) as f64;
let lat_clamped = lat.clamp(GEO_LAT_MIN, GEO_LAT_MAX);
let lon_clamped = lon.clamp(GEO_LON_MIN, GEO_LON_MAX);
let lat_off =
((lat_clamped - GEO_LAT_MIN) / (GEO_LAT_MAX - GEO_LAT_MIN) * cells) as u32;
let lon_off =
((lon_clamped - GEO_LON_MIN) / (GEO_LON_MAX - GEO_LON_MIN) * cells) as u32;
let max = (1u32 << step) - 1;
(lat_off.min(max), lon_off.min(max))
}
fn merge_ranges(sorted: Vec<(u64, u64)>) -> Vec<(f64, f64)> {
let mut out: Vec<(u64, u64)> = Vec::with_capacity(sorted.len());
for (min, max) in sorted {
match out.last_mut() {
Some(prev) if prev.1.saturating_add(1) >= min => {
prev.1 = prev.1.max(max);
}
_ => out.push((min, max)),
}
}
out.into_iter().map(|(a, b)| (a as f64, b as f64)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
const PALERMO: (f64, f64) = (13.361389, 38.115556);
const CATANIA: (f64, f64) = (15.087269, 37.502669);
#[test]
fn geohash_string_matches_redis() {
assert_eq!(&encode_base32_geohash(PALERMO.0, PALERMO.1), b"sqc8b49rny0");
assert_eq!(&encode_base32_geohash(CATANIA.0, CATANIA.1), b"sqdtr74hyu0");
}
#[test]
fn geopos_roundtrip_matches_redis_to_the_last_digit() {
for (lon, lat, want_lon, want_lat) in [
(
PALERMO.0,
PALERMO.1,
"13.36138933897018433",
"38.11555639549629859",
),
(
CATANIA.0,
CATANIA.1,
"15.08726745843887329",
"37.50266842333162032",
),
] {
let score = encode_score(lon, lat).expect("in range");
let (dlon, dlat) = decode_score(score);
assert_eq!(format!("{dlon:.17}"), want_lon, "lon for ({lon},{lat})");
assert_eq!(format!("{dlat:.17}"), want_lat, "lat for ({lon},{lat})");
}
}
}