Skip to main content

oxigdal_noalloc/
geohash.rs

1//! Geohash encoding and decoding for `no_std`, `no_alloc` environments.
2//!
3//! Geohashes encode a (latitude, longitude) pair as a short string using
4//! base32 encoding over interleaved bit streams.
5
6use crate::BBox2D;
7
8/// The standard geohash base32 alphabet.
9pub const BASE32_CHARS: &[u8; 32] = b"0123456789bcdefghjkmnpqrstuvwxyz";
10
11/// A geohash encoded as a fixed-size byte array.
12///
13/// Supports precision 1–12 (matching the standard range).
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct GeoHashFixed {
16    chars: [u8; 12],
17    len: u8,
18}
19
20impl GeoHashFixed {
21    /// Encodes a (latitude, longitude) pair to the given precision (1–12).
22    ///
23    /// If `precision` is outside 1–12, precision is clamped to the valid range.
24    #[must_use]
25    pub fn encode(lat: f64, lon: f64, precision: u8) -> Self {
26        let precision = precision.clamp(1, 12) as usize;
27
28        let mut chars = [0u8; 12];
29        // Interleaved bit encoding: even bits = lon, odd bits = lat
30        let mut min_lat = -90.0_f64;
31        let mut max_lat = 90.0_f64;
32        let mut min_lon = -180.0_f64;
33        let mut max_lon = 180.0_f64;
34
35        let mut is_lon = true; // start with longitude
36        let mut bits = 0u8; // current 5-bit accumulator
37        let mut bit_count = 0u8;
38        let mut char_idx = 0usize;
39
40        // Total bits needed = precision * 5
41        let total_bits = precision * 5;
42
43        for _ in 0..total_bits {
44            if is_lon {
45                let mid = (min_lon + max_lon) * 0.5;
46                if lon >= mid {
47                    bits = (bits << 1) | 1;
48                    min_lon = mid;
49                } else {
50                    bits <<= 1;
51                    max_lon = mid;
52                }
53            } else {
54                let mid = (min_lat + max_lat) * 0.5;
55                if lat >= mid {
56                    bits = (bits << 1) | 1;
57                    min_lat = mid;
58                } else {
59                    bits <<= 1;
60                    max_lat = mid;
61                }
62            }
63            is_lon = !is_lon;
64            bit_count += 1;
65
66            if bit_count == 5 {
67                chars[char_idx] = BASE32_CHARS[bits as usize];
68                char_idx += 1;
69                bits = 0;
70                bit_count = 0;
71            }
72        }
73
74        Self {
75            chars,
76            len: precision as u8,
77        }
78    }
79
80    /// Returns the encoded bytes (ASCII characters of the geohash).
81    #[must_use]
82    #[inline]
83    pub fn as_bytes(&self) -> &[u8] {
84        &self.chars[..self.len as usize]
85    }
86
87    /// Returns the precision of this geohash (1–12).
88    #[must_use]
89    #[inline]
90    pub fn precision(&self) -> u8 {
91        self.len
92    }
93
94    /// Decodes the geohash to the (latitude, longitude) of the cell center.
95    #[must_use]
96    pub fn decode(&self) -> (f64, f64) {
97        let bbox = self.bbox();
98        let lat = (bbox.min_y + bbox.max_y) * 0.5;
99        let lon = (bbox.min_x + bbox.max_x) * 0.5;
100        (lat, lon)
101    }
102
103    /// Returns the bounding box of the geohash cell.
104    ///
105    /// The box is `BBox2D { min_x: min_lon, min_y: min_lat, max_x: max_lon, max_y: max_lat }`.
106    #[must_use]
107    pub fn bbox(&self) -> BBox2D {
108        let mut min_lat = -90.0_f64;
109        let mut max_lat = 90.0_f64;
110        let mut min_lon = -180.0_f64;
111        let mut max_lon = 180.0_f64;
112
113        let bytes = self.as_bytes();
114        let mut is_lon = true;
115
116        for &byte in bytes {
117            // Find the index of this character in BASE32_CHARS
118            let idx = base32_decode_char(byte);
119            // Decode 5 bits, MSB first
120            for bit_pos in (0..5).rev() {
121                let bit = (idx >> bit_pos) & 1;
122                if is_lon {
123                    let mid = (min_lon + max_lon) * 0.5;
124                    if bit == 1 {
125                        min_lon = mid;
126                    } else {
127                        max_lon = mid;
128                    }
129                } else {
130                    let mid = (min_lat + max_lat) * 0.5;
131                    if bit == 1 {
132                        min_lat = mid;
133                    } else {
134                        max_lat = mid;
135                    }
136                }
137                is_lon = !is_lon;
138            }
139        }
140
141        BBox2D::new(min_lon, min_lat, max_lon, max_lat)
142    }
143}
144
145/// Decodes a single base32 character to its 5-bit value.
146/// Returns 0 for unrecognised characters.
147#[inline]
148fn base32_decode_char(c: u8) -> u8 {
149    for (i, &ch) in BASE32_CHARS.iter().enumerate() {
150        if ch == c {
151            return i as u8;
152        }
153    }
154    0
155}