# MapMap platform — dev-experience review

Grounded in building **MapMap Nav**, a full turn-by-turn app, against MapMap's
published SDKs/APIs. Sources: `PLATFORM-GOTCHAS.md`, `FINDINGS.md`,
`NAV-FEATURE-GAPS.md` and the app itself. For the core team.

## 1. Executive summary

The single biggest lever is **stop making mobile integration a bespoke
engineering project**: publish `ai.mapmap:core` to Maven Central, ship a
prebuilt `MapMapKit.xcframework` + CocoaPods podspec, and release an official
`@mapmap/react-native` module seeded from our `modules/mapmap-react-native`.
Everything else — geocode, speed limits, lanes, style clarity — is real but
secondary; none of it matters if a developer can't get past dependency
resolution. Second lever, nearly as costly: the guidance stream is a
one-way, GPS-only firehose with no way to feed it a synthetic or map-matched
location, which forced us to reimplement map-matching, arc-length
route-progress and banner-building entirely in JS (`src/state/guidance/`) —
that's core-SDK logic no integrator should have to rebuild. Fix the
distribution wall and the guidance-injection gap and "an RN dev ships
offline turn-by-turn in an afternoon" becomes true; today it's a multi-day,
native-toolchain-literate undertaking.

## 2. P0 — blocks adoption

**No React Native/Expo SDK exists.** Every RN customer hand-writes a
~2,000-line native bridge (territory lifecycle, routing, guidance events,
voice). *Fix*: publish `@mapmap/react-native` on npm, seeded from
`modules/mapmap-react-native/`.

**Android SDK only resolves from GitHub Packages.** `ai.mapmap:core` needs a
GitHub account and a `read:packages` PAT (`GPR_TOKEN`) even to download —
undocumented, and a hard wall for any dev without repo access. *Fix*: publish
to **Maven Central**; the docs already call it "the next step" — ship it.

**iOS SDK is SwiftPM-only; CocoaPods (i.e. any Expo native module) cannot
consume it.** No supported route exists on Xcode 26 to give a pod an SPM
product dependency (`spm_dependency` is test-specs-only in CocoaPods 1.16).
We were forced to vendor ~150 Swift files plus the `MapMapFFI` and
valhalla-mobile xcframeworks straight into the pod, with source surgery
(import neutralisation, type-name de-collisions, a `Bundle.module` shim) —
see `modules/mapmap-react-native/ios/Vendored/README.md`. *Fix*: ship a
**prebuilt `MapMapKit.xcframework`** (compiled, not source) plus a real
**CocoaPods podspec**. Mapbox and MapLibre both ship this way; it removes
the vendoring exercise for RN, Capacitor, and any non-SPM host.

**No way to inject a simulated or map-matched location into guidance.** The
native SDK drives `startGuidance()` off `trackUserLocation` with no
JS/native hook to feed it a synthetic or externally map-matched position.
Testing, demos, and any app wanting its own location pipeline are locked
out. *Fix*: expose a `feedLocation(latLng, timestamp)` (or similar) entry
point on the guidance API that bypasses the raw GPS listener.

**iOS 0.2.0 shipped without generated UniFFI bindings or the `Resources`
declaration** — ~40 "cannot find type" errors on a clean `swift build`; fixed
in 0.2.1 during this build. *Fix*: run the bindings generator and build the
*packaged* SwiftPM product in CI as a release gate, not just the workspace.

## 3. P1 — big dev-experience wins

- **`/geocode` returns 501** ("Photon not enabled") and there's no on-device
  search or reverse-geocode surface. Tap-to-route can only label a pin with
  raw coordinates. Enable Photon on the gateway and expose
  `GET /geocode/reverse`; add an on-device `search(query, near) -> [Place]`
  over the packaged index via UniFFI.
- **No postal-code search.** `search()` matches POIs/place names but can't
  resolve a postcode ("BN11 1AA"), outcode ("BN11"), or any other country's
  postal code — table stakes for a nav app. We shimmed it client-side:
  postcodes.io for the UK (`ukPostcode.ts`), Zippopotam.us for ~a dozen other
  countries (`postalCode.ts`), merging a hit above the POI results. It's a
  stopgap — MapMap search should resolve postal codes natively and **offline**
  (for installed territories) so both shims and their network dependency can
  be removed.
- **Territories carry no ISO country code.** `TerritorySummary`/`TerritoryInfo`
  expose only `id` + display `name`, so the postal-code shim has to infer the
  country from the territory id/name to pick the right format and provider
  (postal formats collide across borders — US/DE/FR/ES/IT are all five
  digits). Add an ISO 3166-1 `countryCode` (and ideally the bounding box /
  default centre) to the territory contract.
- **No POI category search.** There's no way to query "petrol"/"food"/"ATM"
  by category — search is name/keyword only. The reference app's category
  chips work against hand-tagged mock fixtures; the real SDK needs a
  category/amenity filter (OSM-style tags) on `search()`.
- **Speed limits not in `/route` or the guidance stream (workaround found).**
  The graph now HAS posted limits (OSM `maxspeed` + OpenStreetMapSpeeds
  defaults) and they're reachable per-edge via `POST /trace_attributes`
  (`edges[].speed_limit`, km/h — verified ~100% coverage on UK A-roads). We now
  drive the speed-limit sign + over-limit warning from a per-route
  `/trace_attributes` fetch (`speedLimits.ts`). But that's a whole extra request
  and an arc-length remap: **put `speed_limit` on the `/route` maneuvers (or in
  the guidance/`GuidanceEvent` stream) directly** so clients don't have to
  second-trace their own route. Lanes still have no real data source at all.
- **Safety cameras / enforcement zones not exposed to clients.** #142 extracts
  cameras, enforcement zones, level crossings (rich fields: position,
  `maxspeed_kph`, `direction_deg`, country) but only to offline NDJSON — the
  gateway has no camera/crossing endpoint or tile layer yet. Expose them (tile
  layer or an along-route query) so nav apps can do camera/limit-change alerts.
- **No current-road name in the guidance stream.** The routing engine has
  per-maneuver `street_names`, but the SDK contract drops them, so a
  "current road" banner has nothing authoritative to bind to. We worked
  around it by parsing `street_names` off the raw Valhalla response and
  deriving the current leg's road in our own progress engine (map-tile label
  sampling was unreliable — it missed unnamed residential roads and picked up
  cross-streets). Add `roadName` to `Maneuver` and `currentRoadName` to
  `GuidanceEvent` in the contract so consumers don't each reinvent this.
- **`pmtiles://` in hosted styles breaks MapLibre Native.** The hosted
  "MapMap Light/Dark" styles (`/styles/mapmap-light-…json`) reference a
  `pmtiles://` source that the web SDK's pmtiles plugin understands but
  `maplibre-react-native` cannot load at all. We had to fetch the style and
  rewrite its source to the authenticated XYZ tile endpoint
  (`/tiles/{id}/{z}/{x}/{y}.pbf?api_key=`) client-side — see
  `src/map/onlineStyle.ts`. Publish a native-friendly (XYZ) style variant,
  or document the swap next to the styles API.
- **Interactive tiles share the metered API-call bucket.** One map view
  fires 20–70 concurrent tile requests, blowing the 60 req/min limit
  (429s, blank map) and burning ~74 of a 1,000-call provisional quota per
  view. Meter tile loads separately, on a much higher allowance.
- **Signed offline territories were unavailable** (`/territories` → 404)
  for the whole build, and the ed25519 verifying key is published nowhere —
  blocking MapMap's flagship "navigate in airplane mode" story entirely.
  Fix the channel outage and publish the verifying key in docs (and bundle
  it as a default for the hosted channel).
- **No official `@mapmap/react-native` on npm** (restated from P0 — it's
  also the biggest P1-tier DX multiplier once distribution is unblocked:
  typed events, config plugin, Expo autolinking).

## 4. P2 — polish

- **Docs lag releases**: `/docs/sdks` still shows 0.1.0 and `gpr.key`
  (should be `gpr.token`); no public Android 0.2.0 reference; `/docs/territories`
  shows Rust samples labelled Kotlin/Swift. Version-stamp snippets and lint
  samples against the language they claim.
- **Zoom-expression support matrix.** `<Layer>` paint/layout props with
  nested `["zoom"]` expressions behave inconsistently under
  `maplibre-react-native` — e.g. `icon-size` in
  `src/screens/guidance/RouteArrows.tsx` doesn't reliably re-evaluate live,
  and a `zoom` expression nested inside another expression (e.g.
  `["*", interpolate, factor]`) silently invalidates a layer rather than
  erroring (`src/map/onlineStyle.ts`'s `boostRoadWidths` works around this
  by keeping interpolation as a single top-level expression). Document the
  supported subset for RN explicitly.
- **Route rationale (`rationale: true` on truck routes) is undocumented**
  on `POST /route` despite being a genuine trust-builder.
- **No sample replay corpus** or `.drive.jsonl` schema for turn-by-turn
  testing; we reverse-engineered our own.
- **Undocumented formats**: channel-index JSON schema, installed-package
  on-disk layout.

## 5. What we had to build ourselves — the strongest signal of missing SDK surface

- **Map-matching + arc-length route-progress engine** (`src/state/guidance/routeProgress.ts`,
  `useSimulatedGuidance.ts`, `useLiveGuidance.ts`) — projecting a fix onto
  the route polyline and tracking distance-along as the single source of
  truth for puck position, bearing, distance-to-turn and banner selection.
  This is exactly what a production nav SDK is supposed to own.
- **Manoeuvre-arrow geometry** (`src/screens/guidance/RouteArrows.tsx`) —
  building turn-arrow shapes client-side from route geometry.
- **A smooth chase camera** — the guidance stream ticks at 1 Hz, so any
  camera re-centring on each event visibly hops; we hand-tuned a 3 Hz mock
  tick plus overlapping animation durations to fake smoothness, and had to
  build our own pinch-vs-pan heuristic (`onRegionDidChange` doesn't
  distinguish them) to let a driver zoom without losing follow.
- **Offline-style client rebuild** (`src/map/onlineStyle.ts`) — swapping
  `pmtiles://` for an authenticated XYZ source, re-deriving road-width zoom
  curves, and re-implementing the 3D-building/POI toggle behaviour that the
  hosted style doesn't expose as parameters.

Each of these is generic nav-app plumbing, not MapMap Nav-specific — every
future MapMap integrator will rebuild them unless the SDK grows the surface
to provide them.
