first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
# geohash
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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
```ts
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.