use core::f64::consts::PI;
pub const STEP_MAX: u8 = 26;
pub const LON_MIN: f64 = -180.0;
pub const LON_MAX: f64 = 180.0;
pub const LAT_MIN: f64 = -85.051_128_78;
pub const LAT_MAX: f64 = 85.051_128_78;
const EARTH_RADIUS: f64 = 6_372_797.560_856;
const MERCATOR_MAX: f64 = 20_037_726.37;
const DEG_TO_RAD: f64 = PI / 180.0;
const ALPHABET: &[u8; 32] = b"0123456789bcdefghjkmnpqrstuvwxyz";
pub const HASH_CHARS: usize = 11;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Unit {
M,
Km,
Ft,
Mi,
}
impl Unit {
#[must_use]
pub const fn metres(self) -> f64 {
match self {
Unit::M => 1.0,
Unit::Km => 1000.0,
Unit::Ft => 0.3048,
Unit::Mi => 1609.34,
}
}
#[must_use]
pub fn parse(word: &[u8]) -> Option<Unit> {
let mut buf = [0u8; 2];
if word.len() > 2 {
return None;
}
for (i, b) in word.iter().enumerate() {
buf[i] = b.to_ascii_lowercase();
}
match &buf[..word.len()] {
b"m" => Some(Unit::M),
b"km" => Some(Unit::Km),
b"ft" => Some(Unit::Ft),
b"mi" => Some(Unit::Mi),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Hash {
pub bits: u64,
pub step: u8,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Area {
pub lon: (f64, f64),
pub lat: (f64, f64),
}
impl Area {
#[must_use]
pub fn centre(&self) -> (f64, f64) {
let lon = ((self.lon.0 + self.lon.1) / 2.0).clamp(LON_MIN, LON_MAX);
let lat = ((self.lat.0 + self.lat.1) / 2.0).clamp(LAT_MIN, LAT_MAX);
(lon, lat)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Shape {
pub lon: f64,
pub lat: f64,
pub kind: Kind,
pub unit: Unit,
}
#[derive(Debug, Clone, Copy)]
pub enum Kind {
Circle {
radius: f64,
},
Rect {
width: f64,
height: f64,
},
}
impl Shape {
#[must_use]
pub fn reach(&self) -> f64 {
let m = self.unit.metres();
match self.kind {
Kind::Circle { radius } => radius * m,
Kind::Rect { width, height } => {
let (w, h) = (width / 2.0 * m, height / 2.0 * m);
(w * w + h * h).sqrt()
}
}
}
#[must_use]
fn half(&self) -> (f64, f64) {
let m = self.unit.metres();
match self.kind {
Kind::Circle { radius } => (radius * m, radius * m),
Kind::Rect { width, height } => (width / 2.0 * m, height / 2.0 * m),
}
}
#[must_use]
pub fn bounds(&self) -> Area {
let (width, height) = self.half();
let lat_delta = (height / EARTH_RADIUS) / DEG_TO_RAD;
let top = (width / EARTH_RADIUS / ((self.lat + lat_delta) * DEG_TO_RAD).cos()) / DEG_TO_RAD;
let bottom =
(width / EARTH_RADIUS / ((self.lat - lat_delta) * DEG_TO_RAD).cos()) / DEG_TO_RAD;
let lon_delta = if self.lat < 0.0 { bottom } else { top };
Area {
lon: (self.lon - lon_delta, self.lon + lon_delta),
lat: (self.lat - lat_delta, self.lat + lat_delta),
}
}
#[must_use]
pub fn covers(&self, lon: f64, lat: f64) -> Option<f64> {
match self.kind {
Kind::Circle { radius } => {
let d = distance(self.lon, self.lat, lon, lat);
(d <= radius * self.unit.metres()).then_some(d)
}
Kind::Rect { width, height } => {
let m = self.unit.metres();
if lat_distance(lat, self.lat) > height * m / 2.0 {
return None;
}
if distance(lon, lat, self.lon, lat) > width * m / 2.0 {
return None;
}
Some(distance(self.lon, self.lat, lon, lat))
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Search {
pub boxes: [Hash; 9],
pub bounds: Area,
}
#[must_use]
fn interleave(x: u32, y: u32) -> u64 {
const B: [u64; 5] = [
0x5555_5555_5555_5555,
0x3333_3333_3333_3333,
0x0f0f_0f0f_0f0f_0f0f,
0x00ff_00ff_00ff_00ff,
0x0000_ffff_0000_ffff,
];
let mut x = u64::from(x);
let mut y = u64::from(y);
for (shift, mask) in [(16, B[4]), (8, B[3]), (4, B[2]), (2, B[1]), (1, B[0])] {
x = (x | (x << shift)) & mask;
y = (y | (y << shift)) & mask;
}
x | (y << 1)
}
#[must_use]
fn deinterleave(bits: u64) -> (u32, u32) {
const B: [u64; 6] = [
0x5555_5555_5555_5555,
0x3333_3333_3333_3333,
0x0f0f_0f0f_0f0f_0f0f,
0x00ff_00ff_00ff_00ff,
0x0000_ffff_0000_ffff,
0x0000_0000_ffff_ffff,
];
let mut x = bits;
let mut y = bits >> 1;
for (shift, mask) in [
(0, B[0]),
(1, B[1]),
(2, B[2]),
(4, B[3]),
(8, B[4]),
(16, B[5]),
] {
x = (x | (x >> shift)) & mask;
y = (y | (y >> shift)) & mask;
}
(x as u32, y as u32)
}
#[must_use]
pub fn in_range(lon: f64, lat: f64) -> bool {
(LON_MIN..=LON_MAX).contains(&lon) && (LAT_MIN..=LAT_MAX).contains(&lat)
}
#[must_use]
pub fn encode(lon: f64, lat: f64, step: u8) -> Option<Hash> {
encode_in(lon, lat, step, (LON_MIN, LON_MAX), (LAT_MIN, LAT_MAX))
}
#[must_use]
pub fn encode_in(
lon: f64,
lat: f64,
step: u8,
lon_range: (f64, f64),
lat_range: (f64, f64),
) -> Option<Hash> {
if step == 0 || step > 32 || !in_range(lon, lat) {
return None;
}
let lat_offset = (lat - lat_range.0) / (lat_range.1 - lat_range.0);
let lon_offset = (lon - lon_range.0) / (lon_range.1 - lon_range.0);
let scale = (1u64 << step) as f64;
let bits = interleave((lat_offset * scale) as u32, (lon_offset * scale) as u32);
Some(Hash { bits, step })
}
#[must_use]
pub fn area(hash: Hash) -> Area {
let (ilat, ilon) = deinterleave(hash.bits);
let scale = (1u64 << hash.step) as f64;
let lon_scale = LON_MAX - LON_MIN;
let lat_scale = LAT_MAX - LAT_MIN;
Area {
lon: (
LON_MIN + (f64::from(ilon) / scale) * lon_scale,
LON_MIN + (f64::from(ilon + 1) / scale) * lon_scale,
),
lat: (
LAT_MIN + (f64::from(ilat) / scale) * lat_scale,
LAT_MIN + (f64::from(ilat + 1) / scale) * lat_scale,
),
}
}
#[must_use]
pub const fn align(hash: Hash) -> u64 {
hash.bits << (52 - hash.step * 2)
}
#[must_use]
pub const fn range(hash: Hash) -> (u64, u64) {
let low = align(hash);
let high = align(Hash {
bits: hash.bits + 1,
step: hash.step,
});
(low, high)
}
#[must_use]
pub fn score(lon: f64, lat: f64) -> Option<u64> {
encode(lon, lat, STEP_MAX).map(align)
}
#[must_use]
pub fn decode(raw: f64) -> Option<(f64, f64)> {
if !raw.is_finite() || raw < 0.0 || raw >= (1u64 << 52) as f64 {
return None;
}
let bits = raw as u64;
if bits == 0 {
return None;
}
Some(
area(Hash {
bits,
step: STEP_MAX,
})
.centre(),
)
}
#[must_use]
pub fn lat_distance(lat1: f64, lat2: f64) -> f64 {
EARTH_RADIUS * ((lat2 - lat1) * DEG_TO_RAD).abs()
}
#[must_use]
pub fn distance(lon1: f64, lat1: f64, lon2: f64, lat2: f64) -> f64 {
let v = ((lon2 * DEG_TO_RAD - lon1 * DEG_TO_RAD) / 2.0).sin();
if v == 0.0 {
return lat_distance(lat1, lat2);
}
let (lat1r, lat2r) = (lat1 * DEG_TO_RAD, lat2 * DEG_TO_RAD);
let u = ((lat2r - lat1r) / 2.0).sin();
let a = u * u + lat1r.cos() * lat2r.cos() * v * v;
2.0 * EARTH_RADIUS * a.sqrt().asin()
}
#[must_use]
pub fn steps_for(mut metres: f64, lat: f64) -> u8 {
if metres == 0.0 {
return STEP_MAX;
}
let mut step = 1i32;
while metres < MERCATOR_MAX {
metres *= 2.0;
step += 1;
}
step -= 2;
if !(-66.0..=66.0).contains(&lat) {
step -= 1;
if !(-80.0..=80.0).contains(&lat) {
step -= 1;
}
}
step.clamp(1, i32::from(STEP_MAX)) as u8
}
#[must_use]
fn move_lon(hash: Hash, dir: i8) -> Hash {
if dir == 0 {
return hash;
}
let width = 64 - u32::from(hash.step) * 2;
let mut x = hash.bits & 0xaaaa_aaaa_aaaa_aaaa;
let y = hash.bits & 0x5555_5555_5555_5555;
let zz = 0x5555_5555_5555_5555u64 >> width;
if dir > 0 {
x = x.wrapping_add(zz + 1);
} else {
x |= zz;
x = x.wrapping_sub(zz + 1);
}
x &= 0xaaaa_aaaa_aaaa_aaaau64 >> width;
Hash {
bits: x | y,
step: hash.step,
}
}
#[must_use]
fn move_lat(hash: Hash, dir: i8) -> Hash {
if dir == 0 {
return hash;
}
let width = 64 - u32::from(hash.step) * 2;
let x = hash.bits & 0xaaaa_aaaa_aaaa_aaaa;
let mut y = hash.bits & 0x5555_5555_5555_5555;
let zz = 0xaaaa_aaaa_aaaa_aaaau64 >> width;
if dir > 0 {
y = y.wrapping_add(zz + 1);
} else {
y |= zz;
y = y.wrapping_sub(zz + 1);
}
y &= 0x5555_5555_5555_5555u64 >> width;
Hash {
bits: x | y,
step: hash.step,
}
}
#[must_use]
fn neighbours(hash: Hash) -> [Hash; 8] {
[
move_lat(hash, 1),
move_lat(hash, -1),
move_lon(hash, 1),
move_lon(hash, -1),
move_lon(move_lat(hash, 1), 1),
move_lon(move_lat(hash, 1), -1),
move_lon(move_lat(hash, -1), 1),
move_lon(move_lat(hash, -1), -1),
]
}
#[must_use]
pub fn areas(shape: &Shape) -> Search {
let bounds = shape.bounds();
let mut step = steps_for(shape.reach(), shape.lat);
let Some(mut hash) = encode(shape.lon, shape.lat, step) else {
return Search {
boxes: [Hash { bits: 0, step: 0 }; 9],
bounds,
};
};
let mut near = neighbours(hash);
let short = area(near[0]).lat.1 < bounds.lat.1
|| area(near[1]).lat.0 > bounds.lat.0
|| area(near[2]).lon.1 < bounds.lon.1
|| area(near[3]).lon.0 > bounds.lon.0;
if step > 1 && short {
step -= 1;
hash = encode(shape.lon, shape.lat, step).unwrap_or(hash);
near = neighbours(hash);
}
if step >= 2 {
let own = area(hash);
let zero = Hash { bits: 0, step: 0 };
if own.lat.0 < bounds.lat.0 {
near[1] = zero;
near[7] = zero;
near[6] = zero;
}
if own.lat.1 > bounds.lat.1 {
near[0] = zero;
near[4] = zero;
near[5] = zero;
}
if own.lon.0 < bounds.lon.0 {
near[3] = zero;
near[7] = zero;
near[5] = zero;
}
if own.lon.1 > bounds.lon.1 {
near[2] = zero;
near[6] = zero;
near[4] = zero;
}
}
Search {
boxes: [
hash, near[0], near[1], near[2], near[3], near[4], near[5], near[6], near[7],
],
bounds,
}
}
#[must_use]
pub fn geohash(lon: f64, lat: f64) -> Option<[u8; HASH_CHARS]> {
let hash = encode_in(lon, lat, STEP_MAX, (LON_MIN, LON_MAX), (-90.0, 90.0))?;
let mut out = [b'0'; HASH_CHARS];
for (i, slot) in out.iter_mut().enumerate().take(HASH_CHARS - 1) {
let idx = (hash.bits >> (52 - (i + 1) * 5)) & 0x1f;
*slot = ALPHABET[idx as usize];
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
const PALERMO: (f64, f64, u64) = (13.361389, 38.115556, 3_479_099_956_230_698);
const CATANIA: (f64, f64, u64) = (15.087269, 37.502669, 3_479_447_370_796_909);
#[test]
fn a_point_scores_what_a_real_server_scores_it() {
assert_eq!(score(PALERMO.0, PALERMO.1), Some(PALERMO.2));
assert_eq!(score(CATANIA.0, CATANIA.1), Some(CATANIA.2));
}
#[test]
fn a_score_decodes_back_to_where_the_point_nearly_was() {
let (lon, lat) = decode(PALERMO.2 as f64).expect("a real score decodes");
assert_eq!(format!("{lon}"), "13.361389338970184");
assert_eq!(format!("{lat}"), "38.1155563954963");
let (lon, lat) = decode(CATANIA.2 as f64).expect("a real score decodes");
assert_eq!(format!("{lon}"), "15.087267458438873");
assert_eq!(format!("{lat}"), "37.50266842333162");
}
#[test]
fn a_point_outside_the_projection_has_no_score() {
assert_eq!(score(181.0, 38.0), None);
assert_eq!(score(13.0, 86.0), None);
assert_eq!(score(-180.1, 0.0), None);
assert!(score(180.0, LAT_MAX).is_some());
assert!(score(-180.0, LAT_MIN).is_some());
}
#[test]
fn interleaving_is_its_own_inverse() {
for (x, y) in [(0u32, 0u32), (1, 0), (0, 1), (0x03ff_ffff, 0x0155_5555)] {
assert_eq!(deinterleave(interleave(x, y)), (x, y));
}
}
#[test]
fn the_distances_are_the_ones_a_real_server_answers() {
let d = distance(PALERMO.0, PALERMO.1, CATANIA.0, CATANIA.1);
let (a, b) = (
decode(PALERMO.2 as f64).expect("a score"),
decode(CATANIA.2 as f64).expect("a score"),
);
let stored = distance(a.0, a.1, b.0, b.1);
assert_eq!(format!("{stored:.4}"), "166274.1516");
assert_eq!(format!("{:.4}", stored / Unit::Km.metres()), "166.2742");
assert_eq!(format!("{:.4}", stored / Unit::Mi.metres()), "103.3182");
assert_eq!(format!("{:.4}", stored / Unit::Ft.metres()), "545518.8700");
assert!((d - stored).abs() < 3.0, "{d} against {stored}");
}
#[test]
fn two_points_on_one_meridian_take_the_short_path() {
let a = decode(score(10.0, 40.0).expect("in range") as f64).expect("a score");
let b = decode(score(10.0, 41.0).expect("in range") as f64).expect("a score");
assert_eq!(a.0, b.0);
let d = distance(a.0, a.1, b.0, b.1);
assert_eq!(format!("{d:.4}"), "111226.3808");
assert_eq!(lat_distance(a.1, b.1), d);
}
#[test]
fn the_geohash_strings_are_the_ones_a_real_server_writes() {
let (lon, lat) = decode(PALERMO.2 as f64).expect("a score");
assert_eq!(&geohash(lon, lat).expect("in range"), b"sqc8b49rny0");
let (lon, lat) = decode(CATANIA.2 as f64).expect("a score");
assert_eq!(&geohash(lon, lat).expect("in range"), b"sqdtr74hyu0");
}
#[test]
fn a_unit_is_read_in_any_case_and_nothing_else_is() {
assert_eq!(Unit::parse(b"m"), Some(Unit::M));
assert_eq!(Unit::parse(b"KM"), Some(Unit::Km));
assert_eq!(Unit::parse(b"Ft"), Some(Unit::Ft));
assert_eq!(Unit::parse(b"mI"), Some(Unit::Mi));
assert_eq!(Unit::parse(b"yd"), None);
assert_eq!(Unit::parse(b"meters"), None);
assert_eq!(Unit::parse(b""), None);
}
#[test]
fn a_box_covers_the_scores_of_everything_inside_it() {
let hash = encode(13.0, 38.0, 10).expect("in range");
let (low, high) = range(hash);
let inside = score(13.0, 38.0).expect("in range");
assert!(low <= inside && inside < high);
let next = range(Hash {
bits: hash.bits + 1,
step: hash.step,
});
assert_eq!(high, next.0);
}
#[test]
fn the_step_estimate_shrinks_the_box_as_the_radius_grows() {
assert_eq!(steps_for(0.0, 0.0), STEP_MAX);
assert!(steps_for(1.0, 0.0) > steps_for(1000.0, 0.0));
assert!(steps_for(1000.0, 0.0) > steps_for(1_000_000.0, 0.0));
assert_eq!(steps_for(40_000_000.0, 0.0), 1);
assert_eq!(steps_for(1000.0, 70.0), steps_for(1000.0, 0.0) - 1);
assert_eq!(steps_for(1000.0, 85.0), steps_for(1000.0, 0.0) - 2);
}
#[test]
fn the_neighbours_of_a_box_are_the_eight_boxes_around_it() {
let hash = encode(13.0, 38.0, 10).expect("in range");
let own = area(hash);
let near = neighbours(hash);
assert_eq!(area(near[0]).lat.0, own.lat.1);
assert_eq!(area(near[1]).lat.1, own.lat.0);
assert_eq!(area(near[2]).lon.0, own.lon.1);
assert_eq!(area(near[3]).lon.1, own.lon.0);
assert_eq!(area(near[4]).lat.0, own.lat.1);
assert_eq!(area(near[4]).lon.0, own.lon.1);
}
#[test]
fn the_boxes_around_the_date_line_wrap_rather_than_run_out() {
let hash = encode(179.99, 0.0, 6).expect("in range");
let east = neighbours(hash)[2];
assert!(area(east).lon.0 < area(hash).lon.0);
}
#[test]
fn a_search_keeps_the_boxes_the_area_reaches_and_drops_the_rest() {
let shape = Shape {
lon: 15.0,
lat: 37.0,
kind: Kind::Circle { radius: 200.0 },
unit: Unit::Km,
};
let search = areas(&shape);
assert_ne!(search.boxes[0].bits, 0);
assert!(search.boxes[1..].iter().any(|h| h.bits == 0));
for h in &search.boxes {
if h.bits == 0 && h.step == 0 {
continue;
}
let a = area(*h);
assert!(a.lon.1 >= search.bounds.lon.0 && a.lon.0 <= search.bounds.lon.1);
assert!(a.lat.1 >= search.bounds.lat.0 && a.lat.0 <= search.bounds.lat.1);
}
}
#[test]
fn a_shape_covers_what_is_inside_it_and_nothing_else() {
let circle = Shape {
lon: 15.0,
lat: 37.0,
kind: Kind::Circle { radius: 100.0 },
unit: Unit::Km,
};
assert!(circle.covers(15.0, 37.0).is_some());
assert!(circle.covers(15.0, 37.5).is_some());
assert!(circle.covers(15.0, 39.0).is_none());
let rect = Shape {
lon: 15.0,
lat: 37.0,
kind: Kind::Rect {
width: 200.0,
height: 200.0,
},
unit: Unit::Km,
};
let corner = (15.0 + 1.1, 37.0 + 0.85);
assert!(rect.covers(corner.0, corner.1).is_some());
assert!(circle.covers(corner.0, corner.1).is_none());
}
#[test]
fn a_score_nothing_wrote_does_not_decode() {
assert_eq!(decode(-1.0), None);
assert_eq!(decode(0.0), None);
assert_eq!(decode(f64::NAN), None);
assert_eq!(decode(f64::INFINITY), None);
assert_eq!(decode((1u64 << 52) as f64), None);
assert!(decode(1.5).is_some());
}
}