# 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 ```ts 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 ```ts 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 ```ts 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 ```ts function gridCluster(points: LatLng[], cellSizeMetres: number): LatLng[][] function kMeansCluster(points: LatLng[], k: number, maxIterations?: number): LatLng[][] ``` ### Adapter interfaces ```ts interface GeocodeAdapter { geocode(address: string, options?: GeocodeOptions): Promise reverseGeocode(point: LatLng, options?: ReverseGeocodeOptions): Promise } interface GeocodeResult { address: string point: LatLng confidence: number // 0–1 components: AddressComponents } function createCachedGeocodeAdapter( inner: GeocodeAdapter, options?: CacheOptions ): GeocodeAdapter ``` ### Error codes ```ts 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 ```text 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`.