3.8 KiB
geohash
(no submodules)
Purpose
Encodes a (latitude, longitude) pair into a Geohash base32 string and decodes it back to the centre coordinate of the cell. Geohash is a hierarchical spatial index in which a longer string addresses a smaller bounding box, and any prefix of a hash addresses the enclosing cell — making it useful for proximity bucketing, key-prefix range scans on KV stores, and tiled spatial joins without pulling in a full R-tree or quadkey library.
This implementation follows the canonical Niemeyer/Morton algorithm: alternating-axis bit interleaving (longitude on even bits, latitude on odd) with the Geohash base32 alphabet 0123456789bcdefghjkmnpqrstuvwxyz (intentionally omits a, i, l, o to avoid visual confusion).
Features
encodeGeohash(coord, precision = 9)— produces a base32 string of lengthprecision.decodeGeohash(hash)— returns the centre{ latitude, longitude }of the addressed cell.- Default precision
9(~4.77 m × 4.77 m at the equator) suits typical store-front / asset-tracking use. - Pure, deterministic, allocation-light: a single growing string and integer state.
- Universal — no DOM, no Node, no clock, no RNG.
Usage: Basic Example
import { decodeGeohash, encodeGeohash } from "@suaveplan/geo-earth";
const hash = encodeGeohash({ latitude: 51.5, longitude: -0.1 }, 9);
const coord = decodeGeohash(hash);
// coord ≈ { latitude: 51.5, longitude: -0.1 }
Usage: Advanced Example
import { encodeGeohash } from "@suaveplan/geo-earth";
// Bucket a stream of points by 5-character prefix (~4.9 km × 4.9 km cell)
// for cheap proximity grouping in a key/value store.
function bucketByCell<T extends { latitude: number; longitude: number }>(
points: readonly T[],
precision = 5,
): Map<string, T[]> {
const buckets = new Map<string, T[]>();
for (const p of points) {
const cell = encodeGeohash(p, precision);
const list = buckets.get(cell);
if (list) list.push(p);
else buckets.set(cell, [p]);
}
return buckets;
}
const grouped = bucketByCell([
{ latitude: 51.5, longitude: -0.1 },
{ latitude: 51.51, longitude: -0.09 },
{ latitude: 48.86, longitude: 2.35 },
]);
// grouped has two keys: one shared London bucket and one Paris bucket.
API Reference
encodeGeohash(coord: { latitude: number; longitude: number }, precision?: number): stringprecisiondefaults to9. Each character contributes 5 bits of resolution (~halving cell size every two characters).- Returns a string of exactly
precisioncharacters from the Geohash base32 alphabet.
decodeGeohash(hash: string): { latitude: number; longitude: number }- Returns the centre of the cell addressed by
hash. Round-trip fidelity is bounded by the cell size at the chosen precision; the unit tests asserttoBeCloseToat one decimal for precision-9 round-trips.
- Returns the centre of the cell addressed by
Inputs outside [-90, 90] / [-180, 180] are not validated; callers that accept arbitrary user input should clamp first.
Implementation Notes
- Bit interleaving is implemented with the
evenBitboolean flipping every iteration; on even iterations longitude is bisected, on odd iterations latitude is bisected. This matches the canonical Geohash bit ordering and is what makes string prefixes correspond to enclosing cells. - Base32 lookup uses
BASE32.indexOf(char)per character indecodeGeohash. For very large decode batches a precomputedMap<string, number>would be marginally faster but adds module-load cost; the linear scan is fine for typical workloads at length ≤ 12. - The decoder returns the cell centre, not the bounding box. Callers needing the box (e.g. for tile rendering) can re-run the bisection loop and capture
minLat/maxLat/minLon/maxLonthemselves.