Files
suaveplan/openspec/changes/enterprise-geo/proposal.md
2026-05-27 23:29:53 +01:00

6.2 KiB
Raw Blame History

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

  1. GeoError extends SuaveplanError with six stable codes.
  2. Zod schemas for LatLng, LatLngAlt, BoundingBox, GeocodeResult, AddressComponents, GeocodeOptions.
  3. Vincenty distance — more accurate than Haversine for long distances; falls back to Haversine for antipodal/degenerate inputs.
  4. Cached geocode adapter — wraps any GeocodeAdapter with an in-memory LRU cache (default 1 000 entries, TTL 1 hour).
  5. OTel instrumentation — span geo.geocode with attribute geo.provider; counter geo.geocode_total; histogram geo.geocode_duration_ms.
  6. 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.