# Design: enterprise-ports ## Module layout ``` packages/geospatial/logistics/ports/ package.json tsconfig.lib.json vite.config.ts vitest.config.ts README.md src/ index.ts constants/index.ts # EARTH_RADIUS_NM, DEFAULT_SPEED_KNOTS, VESSEL_CLASS_SPEEDS types/ port.types.ts port.types.md error-codes/ error-codes.ts error-codes.md error-codes.test.ts index.ts schemas/ schemas.ts schemas.md schemas.test.ts index.ts otel/ otel.ts otel.md otel.test.ts index.ts data/ sea-ports-asia/ sea-ports-europe/ sea-ports-americas/ sea-ports-africa-mena/ sea-ports-oceania/ sea-ports/ airports/ rail-terminals/ all-ports/ shipping-lanes/ distance/ distance.ts # great-circle wrapper, nm/km conversions distance.md distance.test.ts index.ts find-port/ find-port.ts find-port.md find-port.test.ts index.ts nearest-port/ nearest-port.ts nearest-port.md nearest-port.test.ts index.ts spatial-index/ spatial-index.ts spatial-index.md spatial-index.test.ts index.ts lane-lookup/ lane-lookup.ts lane-lookup.md lane-lookup.test.ts index.ts lane-distance/ lane-distance.ts lane-distance.md lane-distance.test.ts index.ts lane-progress/ lane-progress.ts lane-progress.md lane-progress.test.ts index.ts eta/ eta.ts eta.md eta.test.ts index.ts property.test.ts ``` ## Distance helper `distanceNm(a, b)` and `distanceKm(a, b)` wrap `@suaveplan/geo`'s `greatCircleDistance` against a WGS84-radius spherical earth (semi-major axis 6_371_000 m) via `defineEllipsoid`. The genesis `greatCircleDistance` returns metres by default; this package converts via the geo `convertDistance` utility. We bind a single `EARTH_SPHERE` ellipsoid once at module load — no allocation per call. ## Error mapping Each main-branch `new PortError("msg", "CODE")` call maps to a typed subclass: | main code | genesis class | |---|---| | `NO_PORTS` | `PortsNoPortsError` | | `INVALID_RADIUS` | `PortsInvalidRadiusError` | | `INSUFFICIENT_WAYPOINTS` | `PortsInsufficientWaypointsError` | | `INVALID_SPEED` | `PortsInvalidSpeedError` | | `MISSING_SPEED` | `PortsMissingSpeedError` | | _(new)_ | `PortsPortNotFoundError` — strict variant of `findPort` | | _(new)_ | `PortsLaneNotFoundError` — strict variant of `getShippingLane` | | _(new)_ | `PortsInvalidLaneError` — Zod schema rejected | All extend `PortsError extends SuaveplanError` with i18n message keys. ## Spatial index `PortSpatialIndex` keys ports by `floor(latitude / 5)` and `floor(longitude / 5)` into a `Map`. Query for a coordinate fetches the 3×3 cell neighbourhood and runs exact great-circle distance on the candidate set. For ~120 curated ports this is overkill in absolute terms but keeps the public API stable as the dataset grows. ## OTel instruments Module-scope meter `@suaveplan/ports@0.1.0`: - Counter `ports.lookup_total` — labels `kind=findPort|byCountry|byType|search`. - Counter `ports.lane_lookup_total` — labels `mode=sea|rail|air|all`. - Counter `ports.eta_total` — labels `confidence=high|medium|low`. - Histogram `ports.find_nearest_duration_ms`. - Histogram `ports.lane_distance_duration_ms`. - Histogram `ports.eta_duration_ms`. `refreshPortsMeters()` re-resolves every instrument against the current global `MeterProvider` after tests wire in-memory exporters. ## Clock injection `estimateETA(coord, lane, speedKnotsOverride?, opts?)`: `opts = { departureTime?: Date; clock?: Clock }`. If `departureTime` is omitted and `clock` is supplied → `new Date(clock.now())`; else `new Date()`. Tests pass a `FakeClock` from `@suaveplan/testing` for determinism. ## Data shape stability The migrated dataset preserves every record from the main branch (coordinates, throughputs, vessel limits, IATA/UIC codes) — adding any new fields would break consumer expectations. The data is split into per-region files exactly mirroring the main layout so future curated updates can be diffed cleanly.