# MapMap platform — gotchas, problems & fixes

Everything that tripped up building **MapMap Nav** as an outside customer,
turned into an actionable punch-list for the MapMap team. Grouped by area,
each item is **Problem → Evidence → Fix**, with a severity:

- 🔴 **blocker** — stops a customer cold
- 🟠 **major** — costs days or forces an ugly workaround
- 🟡 **minor** — friction, papercuts, docs

Status as of 17 Jul 2026, ~22:00. Items already fixed this session are
marked ✅.

---

## 1. SDK distribution & packaging — the biggest cost

### 1.1 🔴 There is no React Native / Expo module
- **Problem**: MapMap ships native Swift + Kotlin SDKs only. Any RN/Expo/
  Capacitor/Flutter customer must hand-write a bridge — ~2,000 lines across
  both platforms (territory lifecycle, routing, a guidance event stream,
  voice, typed events).
- **Evidence**: this entire project. The bridge is real work and a senior
  mobile task on its own.
- **Fix**: ship an official **`@mapmap/react-native`** (Expo module +
  config plugin). Our `modules/mapmap-react-native` is a working first
  draft to seed it. This is the single highest-leverage thing MapMap can
  build — it turns "a determined shop forces it to work in a month" into
  "any RN dev ships offline turn-by-turn in an afternoon".

### 1.2 🔴 The published iOS SDK 0.2.0 did not compile ✅ (fixed → 0.2.1)
- **Problem**: `mapmap-ios` 0.2.0 shipped **without its generated UniFFI
  Swift bindings** (`Sources/MapMapKit/Generated/*.swift`, gitignored build
  products) and without the `Resources` declaration for the earcon assets.
  Result: ~40 "cannot find type" errors; the package was unusable as
  distributed.
- **Evidence**: `swift build` on a blank consumer → BUILD FAILED.
- **Fix (done)**: regenerated the bindings from the 0.2.0 core, added
  `resources: [.process("Resources")]`, cut **0.2.1**, verified an
  unauthenticated consumer `swift package resolve` + compile end-to-end.
- **Prevent recurrence**: the publish workflow must (a) run the bindings
  generator before packaging, (b) build the *packaged* SwiftPM product in
  CI as a smoke test (not just the workspace), so a missing file fails the
  release, not the customer.

### 1.3 🟠 CocoaPods ↔ SwiftPM wall for the iOS SDK
- **Problem**: an Expo module is a CocoaPods pod, and a pod cannot consume a
  SwiftPM product. `spm_dependency` is test-specs-only in CocoaPods 1.16;
  injecting an `XCRemoteSwiftPackageReference` into the Pods project leaves
  the module unresolved. The only route to green was **vendoring the entire
  SDK + Valhalla + boost into the pod and recompiling it** — days of work,
  and fragile.
- **Evidence**: `no such module 'MapMapKit'` until fully vendored.
- **Fix**: publish a **prebuilt `MapMapKit.xcframework`** (all slices) **and
  a CocoaPods podspec**, so integrators link a binary instead of vendoring
  and recompiling Valhalla+boost. Mapbox/MapLibre both ship this way.

### 1.4 🔴 Android SDK requires a personal GitHub PAT
- **Problem**: `ai.mapmap:core` is on **GitHub Packages**, which needs a
  GitHub account **and** a `read:packages` PAT — even to download. Nothing
  in the docs tells an Android customer this. Compare `npm install`.
- **Evidence**: unauthenticated Maven resolve → 401.
- **Fix**: publish to **Maven Central** (the docs already call it "the next
  step"). No account, no PAT, standard `implementation "ai.mapmap:core"`.

### 1.5 🟠 Repo/artefact access depends on private-repo membership
- **Problem**: while the source repo is private, iOS SPM release-asset URLs
  404 unauthenticated and GitHub Packages needs repo access, so *no outside
  developer* can install either mobile SDK.
- **Fix**: host the iOS binary on a **public** distribution repo (done for
  the demo: `Mapmapai/mapmap-ios`) and move Android to Maven Central (1.4).

---

## 2. Backend / gateway

### 2.1 🔴 Signed territory channel returns 404
- **Problem**: `GET /territories` (and `index.sig`, manifests) → 404 "no
  such territory channel object". **No customer can install any offline
  territory on any platform** — which is MapMap's flagship "navigate in
  airplane mode" story.
- **Evidence**: authenticated 404 on every channel path; rendering tiles
  (`tiles.mapmap.ai`) are fine, so it's the channel store specifically —
  looks displaced by the world build / `/opt` migration.
- **Fix**: republish the signed channel index + packages to the path the
  gateway serves; add a CI/uptime check that `GET /territories` returns a
  valid signed index, so this outage is caught, not customer-reported.

### 2.2 🔴 Territory verifying key is unpublished
- **Problem**: docs say the ed25519 channel verifying key must be "baked in
  at build time", but the hosted channel's public key is **published
  nowhere**. Without it a customer cannot verify signed territories at all.
- **Fix**: publish the channel's verifying key (hex) in the docs and, ideally,
  bundle it as a default in the SDK for the hosted channel, with an override
  for self-hosters.

### 2.3 🟠 Interactive vector tiles are unusable on a metered key
- **Problem**: a single map view fires 20–70 tile requests concurrently.
  On a self-serve key that (a) trips the 60 req/min limit → **429s, blank
  map**, and (b) burns **~74 metered calls per view** (one view = 7% of the
  provisional 1,000-call budget; a verified 50k key = ~600 views).
- **Evidence**: `Failed to load tile … HTTP status code 429`; quota jumped
  208→282 in one render.
- **Fix**: meter **tile loads separately** from API calls, on a much higher
  (or unmetered) allowance — the Mapbox/MapTiler model. Interactive maps and
  a per-call routing quota can't share one 60/min bucket. (Or steer online
  users to a tile CDN and reserve the metered API for routing.)

### 2.4 🟠 Hosted routing ignored the request origin ✅ (fixed by redeploy)
- **Problem**: every `POST /route` pinned the origin to one fixed edge
  ("Slade Lane, Manchester") regardless of input — so all routes and the
  public `/map` directions were wrong.
- **Evidence (earlier)**: London→Bank (3 km) returned a 320 km
  Manchester→London route.
- **Fix (observed)**: a gateway redeploy resolved it — London→Bank now
  3.68 km with a correct central-London first maneuver. **Add a smoke test**
  (a known short route with an assert on distance + first-maneuver locality)
  so an origin-snapping regression can't ship silently again.

### 2.5 🟠 `/geocode` returns 501 (Photon not enabled)
- **Problem**: `GET /geocode` → 501 "operator must set SN_PHOTON_URL". So
  there's **no forward/reverse geocoding** for customers — search has to be
  faked or BYO. (On-device search also isn't exposed; see 3.3.)
- **Fix**: stand up the Photon container on the hosted gateway and set
  `SN_PHOTON_URL`; document the endpoint as live.

### 2.6 🟠 Gateway had a hard 502 outage mid-session
- **Problem**: `api.mapmap.ai` (health, styles, tiles proxy) returned 502
  for a stretch; recovered on its own after a few minutes.
- **Fix**: front the gateway with a health-checked restart/supervisor and an
  uptime monitor (status.mapmap.ai already exists — wire this endpoint into
  it) so a crash self-heals and is visible.

---

## 3. SDK API gaps (found while bridging)

### 3.1 🟡 No offline-style API
- **Problem**: no SDK call returns a MapLibre style for an installed
  territory. Docs only show the web `buildStyle()` and two JSON transforms
  that presuppose a style. `getTerritoryStyle` had to resolve style JSON off
  disk from inside the package.
- **Fix**: expose `territoryStyle(id, theme) -> String` (offline, local
  refs) on the native SDKs, mirroring the web SDK.

### 3.2 🟡 No documented territory-removal API
- **Fix**: expose `TerritoryStore.remove(id)` (or document it if it exists).

### 3.3 🟡 No on-device search surface
- **Problem**: packages bundle geocode indices but the native SDKs expose no
  search API, and hosted `/geocode` is 501 (2.5). A customer has *no* search
  path today.
- **Fix**: expose on-device search over the packaged index via UniFFI
  (`search(query, near) -> [Place]`), and/or enable Photon (2.5).

### 3.4 🟡 `VoiceGuidance` has no "speaking" signal
- **Problem**: exposes `muted`/`volume` but no way to know when it's
  actively speaking, so UIs can't reflect speaking state accurately.
- **Fix**: emit a `speaking` boolean (start/stop callbacks) on VoiceGuidance.

### 3.5 🟡 No structured route from the SDK
- **Problem**: `RouteResult` exposes only `valhallaResponseJson`; every
  integrator re-parses Valhalla JSON for geometry/maneuvers.
- **Fix**: parse once in the SDK and expose typed `geometry`/`maneuvers`.

### 3.6 🟡 minSdk 26 is undocumented and above Expo's default
- **Fix**: state the Android minSdk (26) prominently; it forces an app bump.

### 3.7 🟡 Gradle trap for module authors
- **Problem**: a module-local `repositories {}` block is silently ignored —
  the GitHub Packages repo must be injected at the **root** project or
  dependency resolution never searches it.
- **Fix**: document this in the Android SDK integration guide (it cost real
  debugging time).

---

## 4. Documentation

### 4.1 🟠 Docs lag the release
- `/docs/sdks` still documents **0.1.0** and calls the credential property
  **`gpr.key`** (the working name is `gpr.token`).
- The `sdk-v0.2.0` GitHub release was **iOS-only**; no public Android 0.2.0
  API reference or release notes.
- `/docs/territories` shows **Rust** sample code labelled "Kotlin/Swift".
- **Fix**: gate docs on release (version-stamp snippets), add an Android API
  reference, and lint code samples for the language they claim to be.

### 4.2 🟡 Undocumented formats
- The **channel-index JSON schema**, the **installed-package on-disk
  layout**, and the **`.drive.jsonl` replay-corpus line schema** are all
  unpublished — each had to be reverse-engineered. Ship a sample corpus.

### 4.3 🟡 Stale `weareprecode` references after the Mapmapai org move
- Git redirects mask it, but hand-typed Maven/asset URLs don't. Sweep docs
  and llms.txt for `weareprecode`.

---

## 5. Publishing / CI / infra

### 5.1 🟠 GitHub Actions org billing failed (twice) — blocked all publishing
- **Problem**: all workflows failed instantly with 0 steps ("payments
  failed or spending limit"); macOS runs bill 10×. No SDK could publish
  until billing was restored.
- **Fix**: a prepaid buffer + a billing/quota alert; consider self-hosted
  macOS runners for the 10×-billed iOS jobs.

### 5.2 🟡 npm republish needs a version bump
- **Problem**: re-tagging without bumping `package.json` → npm 403 "cannot
  publish over previously published version".
- **Fix**: the release workflow should bump (or assert a bumped) version
  before `npm publish`.

### 5.3 🟡 Duplicate migration nearly shipped
- Two different `0007_*.sql` migrations existed on different branches (same
  sqlx version number) — a merge-time collision. **Fix**: a CI check that
  migration version numbers are unique across the repo.

---

## 6. Not MapMap's fault — but worth knowing

### 6.1 🟠 Expo SDK 57 / RN 0.86 don't build on the Xcode 26.0.1 seed
- `weak let` concurrency errors across ExpoModulesJSI/Core, then an Expo
  umbrella-header failure. A pre-release-toolchain issue, not MapMap's — but
  if MapMap ships `@mapmap/react-native` (1.1) it should pin/test against a
  known-good Xcode and document it, so customers don't attribute the failure
  to MapMap.

---

## 7. More gaps, found building the deeper nav features

These surfaced while adding tap-to-route, truck/ADR, isochrones, lanes and
the layers control — all things a real nav app needs.

### 7.1 🟠 No reverse geocoding — "tap the map" can only show coordinates
- **Problem**: tap-anywhere-to-route is table stakes, but with no reverse
  geocode the app can only label the point "Dropped pin, 51.507, -0.128".
  `/geocode` (and reverse) is 501 (Photon not enabled — §2.5), and the SDK
  exposes no on-device reverse lookup.
- **Fix**: enable Photon and expose `GET /geocode/reverse`; also surface
  on-device reverse over the packaged index for offline.

### 7.2 🟠 No speed-limit data in guidance
- **Problem**: `GuidanceEvent` carries speed but no speed **limit**, so the
  classic speed-limit roundel can't be populated — a feature every driver
  expects. We built the UI; it stays hidden until the SDK provides a value.
- **Fix**: emit `speedLimitKph` on the guidance event (the tiles carry
  `maxspeed`; surface it through the SDK/engine).

### 7.3 🟡 Lane data isn't surfaced through the SDK
- **Problem**: the OSRM shim can return lane arrows (`banner_instructions`),
  but the native SDK / bridge contract has no lane field, so our lane bar is
  mock-driven. 
- **Fix**: thread lane data from the engine to the guidance event.

### 7.4 🟠 Hosted styles use `pmtiles://`, which MapLibre Native (RN) can't load
- **Problem**: the hosted "MapMap Light/Dark" styles
  (`/styles/mapmap-light-…`) reference `pmtiles://…uk.pmtiles`. That scheme
  works in the web SDK (with the pmtiles JS plugin) but **not in
  maplibre-react-native**. We had to fetch the style and swap its source to
  the authenticated XYZ endpoint (`/tiles/{id}/{z}/{x}/{y}.pbf?api_key=`) —
  reverse-engineering that mapping cost time.
- **Fix**: publish a native-friendly style variant (XYZ source), or document
  the "swap the source for RN/native" recipe next to the styles API.

### 7.5 🟡 Truck route rationale isn't discoverable
- **Problem**: the delight roadmap added `rationale: true` for truck routes
  ("avoided a 3.5 m height limit"), a great trust-builder, but it isn't in
  the public route docs, so an integrator won't find it.
- **Fix**: document `rationale` on `POST /route` with an example response.

### 7.6 🟡 News-image pipeline needs Higgsfield
- **Problem**: publishing a `/news` article requires a `…-baked.webp` card
  image; the generator (`npm run news:image`) depends on Higgsfield, so an
  outside contributor can't produce one. (We used a real screenshot instead.)
- **Fix**: allow a plain image (or a scripted local fallback) for the card.

### 7.7 🟡 Chase-camera smoothness is bounded by the guidance event rate
- **Problem**: the SDK/mock emits guidance at 1 Hz; a chase camera re-centres
  on each event, so at 1 Hz the motion visibly hops once a second no matter
  how the tween is tuned. Raising the mock to ~3 Hz (step size scaled by the
  interval, so real speed is unchanged) plus a camera-animation duration kept
  just above the tick interval (so animations overlap and never stall) made
  it glide.
- **Fix (platform)**: the real native SDK should emit interpolated location
  at ≥3–5 Hz during guidance (or expose a client-side interpolator), so
  consumers get smooth follow without hand-tuning tick rates.

### 7.8 🟡 No first-class "pinch-zoom while keeping chase" in the map API
- **Problem**: MapLibre RN's `onRegionDidChange` only reports `userInteraction`
  + the settled view state — it doesn't distinguish a pinch from a pan. To let
  a driver zoom without losing follow, we had to infer intent (centre still
  near the puck ⇒ pinch, keep chasing and store a zoom offset; large centre
  drift ⇒ pan, release follow). Works, but it's app-side heuristics.
- **Fix (platform)**: a follow-with-user-zoom mode (Google/Apple-style: pinch
  adjusts zoom, camera keeps tracking; a pan releases) would remove the guesswork.

## Priority order (what to fix first)

1. **Territory channel 404** (2.1) + **verifying key** (2.2) — restores the
   flagship offline story for everyone.
2. **`@mapmap/react-native`** (1.1) + **prebuilt xcframework/podspec** (1.3)
   + **Maven Central** (1.4) — removes the entire mobile-integration cliff.
3. **Tile metering** (2.3) + **Photon/geocode** (2.5) — makes online maps
   and search actually usable.
4. **Release hygiene**: package-build smoke test (1.2), route smoke test
   (2.4), docs-on-release (4.1), billing alert (5.1).

Fix 1–2 and MapMap goes from "impressive but hard to adopt" to "an RN dev
ships offline turn-by-turn in an afternoon" — which is the whole pitch.
