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:
- Bundled overview (always). A z≤9 Malaysia extract (~5.3 MB) ships in
APK assets under
assets/maps/and is copied once tofilesDir/maps/on first use —pmtiles://asset://does not work because PMTiles needs byte-range reads thatAssetManagerFileSourcecannot provide. - Imported full pack (opt-in). Settings › Backup & data gains "Offline map pack":
the user picks any
.pmtilesdocument; 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). - 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).
.pmtilescountry packs are never committed to git; only the small bundled overview asset is.maplibre-composeis 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.