Files
skills/docs/geospatial/geo/geo-earth/src/geohash/geohash.md
2026-05-29 08:20:56 +01:00

3.8 KiB
Raw Blame History

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 length precision.
  • 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): string
    • precision defaults to 9. Each character contributes 5 bits of resolution (~halving cell size every two characters).
    • Returns a string of exactly precision characters 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 assert toBeCloseTo at one decimal for precision-9 round-trips.

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 evenBit boolean 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 in decodeGeohash. For very large decode batches a precomputed Map<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/maxLon themselves.