6.2 KiB
OpenSpec Change: enterprise-geo
- Change ID: enterprise-geo
- Package:
@suaveplan/geo - Category: universal
- Target path:
universal/geo/geo - Status: IN PROGRESS
- Date: 2026-04-24
- Reference spec:
/mnt/s/web/suaveplan/enterprise/geo.md
Summary
Deliver @suaveplan/geo on the genesis branch from scratch at enterprise quality. The package provides geolocation and geospatial utilities: WGS84 coordinate types, Haversine / Vincenty distance calculation, bearing and midpoint computation, bounding-box construction and intersection, GeoJSON feature/geometry construction helpers, point-in-polygon (ray-casting), polygon area calculation, clustering helpers (k-means, grid), geocoding adapter interface with in-memory cache, reverse-geocoding adapter interface, and elevation lookup interface. The main-branch version sat at 99.38% coverage. Genesis adds structured errors, Zod schemas, OTel instrumentation, and property-based tests.
Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build.
Available workspace deps on genesis
| Package | Role |
|---|---|
@suaveplan/error (workspace:^) |
GeoError; GEO.* error codes |
@suaveplan/types (workspace:^) |
LoggerLike, Clock |
@suaveplan/retry (workspace:^) |
Retry for geocoding adapter network calls |
zod (catalog:forms) |
Schema validation |
@opentelemetry/api (catalog:observability) |
Spans + counters |
Dev deps: vitest, @vitest/coverage-istanbul, fast-check (all catalog:testing).
Public API surface
Coordinate types
interface LatLng {
lat: number // degrees, -90 to 90
lng: number // degrees, -180 to 180
}
interface LatLngAlt extends LatLng {
alt?: number // metres above sea level
}
interface BoundingBox {
north: number
south: number
east: number
west: number
}
Distance and geometry
function haversineDistance(a: LatLng, b: LatLng): number // metres
function vincentyDistance(a: LatLng, b: LatLng): number // metres (more accurate)
function bearing(from: LatLng, to: LatLng): number // degrees 0–360
function midpoint(a: LatLng, b: LatLng): LatLng
function destination(from: LatLng, bearing: number, distance: number): LatLng
function boundingBoxFromPoints(points: LatLng[]): BoundingBox
function expandBoundingBox(box: BoundingBox, metres: number): BoundingBox
function boundingBoxesIntersect(a: BoundingBox, b: BoundingBox): boolean
function pointInBoundingBox(point: LatLng, box: BoundingBox): boolean
GeoJSON helpers
function toGeoJsonPoint(point: LatLng): GeoJSON.Point
function toGeoJsonLineString(points: LatLng[]): GeoJSON.LineString
function toGeoJsonPolygon(rings: LatLng[][]): GeoJSON.Polygon
function fromGeoJsonPoint(point: GeoJSON.Point): LatLng
function polygonArea(ring: LatLng[]): number // square metres (shoelace + WGS84 correction)
function pointInPolygon(point: LatLng, ring: LatLng[]): boolean // ray-casting
Clustering
function gridCluster(points: LatLng[], cellSizeMetres: number): LatLng[][]
function kMeansCluster(points: LatLng[], k: number, maxIterations?: number): LatLng[][]
Adapter interfaces
interface GeocodeAdapter {
geocode(address: string, options?: GeocodeOptions): Promise<GeocodeResult[]>
reverseGeocode(point: LatLng, options?: ReverseGeocodeOptions): Promise<GeocodeResult>
}
interface GeocodeResult {
address: string
point: LatLng
confidence: number // 0–1
components: AddressComponents
}
function createCachedGeocodeAdapter(
inner: GeocodeAdapter,
options?: CacheOptions
): GeocodeAdapter
Error codes
type GeoErrorCode =
| 'GEO_INVALID_COORDINATE'
| 'GEO_INVALID_POLYGON'
| 'GEO_GEOCODE_FAILED'
| 'GEO_REVERSE_GEOCODE_FAILED'
| 'GEO_CLUSTER_FAILED'
| 'GEO_ABORTED'
Genesis additions
GeoErrorextendsSuaveplanErrorwith six stable codes.- Zod schemas for
LatLng,LatLngAlt,BoundingBox,GeocodeResult,AddressComponents,GeocodeOptions. - Vincenty distance — more accurate than Haversine for long distances; falls back to Haversine for antipodal/degenerate inputs.
- Cached geocode adapter — wraps any
GeocodeAdapterwith an in-memory LRU cache (default 1 000 entries, TTL 1 hour). - OTel instrumentation — span
geo.geocodewith attributegeo.provider; countergeo.geocode_total; histogramgeo.geocode_duration_ms. - Property-based tests — distance symmetry:
haversineDistance(a,b) === haversineDistance(b,a); bounding-box expansion monotonicity; point-in-polygon edge cases (point on edge, winding direction).
File layout
universal/geo/geo/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
constants/
index.ts # EARTH_RADIUS_M, DEG_TO_RAD
types/
common.ts
common.md
distance/
haversine/
haversine.ts
haversine.md
haversine.test.ts
index.ts
vincenty/
vincenty.ts
vincenty.md
vincenty.test.ts
index.ts
geometry/
bounding-box/
bounding-box.ts
bounding-box.md
bounding-box.test.ts
index.ts
polygon/
polygon.ts
polygon.md
polygon.test.ts
index.ts
geojson/
geojson.ts
geojson.md
geojson.test.ts
index.ts
clustering/
grid-cluster/
grid-cluster.ts
grid-cluster.md
grid-cluster.test.ts
index.ts
kmeans-cluster/
kmeans-cluster.ts
kmeans-cluster.md
kmeans-cluster.test.ts
index.ts
geocoding/
geocode-adapter/
geocode-adapter.ts
geocode-adapter.md
geocode-adapter.types.ts
geocode-adapter.test.ts
index.ts
cached-geocode/
cached-geocode.ts
cached-geocode.md
cached-geocode.test.ts
index.ts
__benchmarks__/
geo.bench.ts
tests/
integration.test.ts
property.test.ts
Impact
Downstream consumers: @suaveplan/ports, @suaveplan/tides, @suaveplan/heightmap-tiles, @suaveplan/osm-buildings.
Workspace dep prerequisites: @suaveplan/error, @suaveplan/types, @suaveplan/retry.