Skip to content

ADR-0030: Offline vector map pack for the location picker

Context

ADR-0026 shipped a tile-free canvas grid picker because ADR-0015 rules out network I/O — an online tile map or geocoder is not used. The canvas works but gives no geographic context: no streets, no neighborhoods, no coastlines. Users placing or reading a transaction location deserve a real basemap, still with zero network.

Decision

The location picker renders offline vector tiles from a single-file PMTiles archive, with a three-tier fallback:

  1. Bundled overview (always). A z≤9 Malaysia extract (~5.3 MB) ships in APK assets under assets/maps/ and is copied once to filesDir/maps/ on first use — pmtiles://asset:// does not work because PMTiles needs byte-range reads that AssetManagerFileSource cannot provide.
  2. Imported full pack (opt-in). Settings › Backup & data gains "Offline map pack": the user picks any .pmtiles document; the header is validated (Pmtiles.isV3Header) while streaming into place atomically. Provenance (display name) lives in the encrypted settings table (KEY_MAP_PACK_NAME). We publish dated whole-Malaysia packs as GitHub release artifacts cut from Protomaps' public daily builds (scripts/cut-map-pack.sh, z0–13 ≈ 114 MB measured).
  3. Canvas fallback (ADR-0026 unchanged) when even the overview is unavailable.

Rendering uses MapLibre Native through the official maplibre-compose wrapper plus its Android OpenGL runtime artifact, which supplies native pmtiles://file:// support. The pin stays fixed at screen center — pan/zoom moves the target, exactly the ADR-0026 drag interaction translated to a real map. Style JSON, glyphs (Noto Sans Regular, OFL), and attribution ship in assets; nothing loads over the network.

Consequences

  • No app code loads map data over the network: the release manifest carries MapLibre's network permissions (ADR-0015 Amendment), but map data reaches devices only via the APK (overview) or user-initiated file import (full pack).
  • Attribution "© OpenStreetMap contributors" is embedded in the style's source and always rendered by MapLibre's overlay (ODbL compliance).
  • Full-pack zoom is capped at z14 client-side; the published pack targets z0–13 (~114 MB measured for the MY bbox; z14 ≈ 226 MB, z15 ≈ 456 MB).
  • .pmtiles country packs are never committed to git; only the small bundled overview asset is.
  • maplibre-compose is pre-1.0; upgrades may require code motion. The packaged OpenGL runtime keeps API 26+ devices and emulators covered; the Vulkan runtime remains an alternative for hardware that prefers it.
  • The canvas picker remains reachable (Loading / CanvasFallback states), so the feature degrades gracefully on any device.

Amendment (2026-10-19): merged network permissions

The 0.20 MapLibre runtime merges INTERNET and ACCESS_NETWORK_STATE into the release manifest; the app itself declares ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION. ACCESS_WIFI_STATE was part of the older bundled SDK and the 0.20 Android runtime no longer declares it — see ADR-0015; the app declares no removal rule (full trace in ADR-0015).

Rendering remains offline: the picker reads only the APK's bundled PMTiles overview or a user-imported .pmtiles file, and the style/glyphs/attribution are assets. The release gate scripts/check-release-permissions.sh pins the merged permission set so a MapLibre or dependency bump cannot add capabilities silently.

Amendment (2026-10-31): the style asset loads off Main

OfflineMapStyle.loadAsync reads and token-resolves the bundled style asset on the IO dispatcher and caches the result per tile URL/theme. The location picker and the passive editor preview collect it through produceState, so opening a located transaction no longer pays an asset read on the first frame (#335).

Alternatives considered

  • Online tiles behind an opt-in permission — rejected: reverses ADR-0015's core guarantee and reopens the Play-review surface.
  • Raster MBTiles + osmdroid — rejected: ~340 MB at z12 alone, ~22 GB at z15 for the same bbox, fixed style, blurry between zooms.
  • Mapsforge prebuilt maps — viable renderer but weaker Compose story and a second ecosystem to maintain; MapLibre + PMTiles won on format (single-file, cloud-friendly) and active maintenance.
  • Bundle the full pack in the APK / Play Asset Delivery — rejected: couples map data to app releases, bloats every install by hundreds of MB, and adds CI complexity for a closed-testing-scale app.