# Mobile maps with MapLibre Native

MapMap's mobile SDKs are deliberately headless. `ai.mapmap:core` on
Android and `MapMapKit` on iOS give you routing, guidance, voice and
signed offline territory packages, and they hand back a **MapLibre style
JSON** for you to render. Neither ships a map view. The renderer is
[MapLibre Native](https://github.com/maplibre/maplibre-native), the
open-source C++ map engine with iOS and Android bindings, and you own it.

That is the same split the web takes, where `@mapmap/maps` wraps
`maplibre-gl` (see [Maps, tiles & Studio](/docs/maps)). On mobile there is
no wrapper: you add MapLibre Native yourself and point it at a MapMap
style. This page is how.

It covers three things people get wrong on the first attempt: which style
URL to use, what kind of API key works in a native app (an
origin-restricted key does **not**, and the reason is worth reading), and
what happens when the radios are off.

> **Scope.** Everything here is the map surface. Turn-by-turn guidance,
> ADR-aware offline routing and territory installation live in the
> platform SDKs: [SDKs & installation](/docs/sdks),
> [Android API reference](/docs/android),
> [Territory packages & OTA updates](/docs/territories). What MapMap does
> not ship for mobile is stated plainly in
> [What we do not ship](#what-we-do-not-ship) at the end, before you build
> a plan around something that is not there.

## Which map source

There are three, and they are not interchangeable.

| Source | URL or call | Key | Metered | Use it for |
| --- | --- | --- | --- | --- |
| Hosted house style | `https://api.mapmap.ai/styles/{id}.json` | none | no | Evaluation, demos, prototypes |
| Keyed tile API | `https://api.mapmap.ai/tiles/{territory}/style.json?api_key=…` | yes | yes | Production apps online |
| Installed territory | `TerritoryStore.territoryStyle(…)` | none | no | Production apps offline |

### Hosted house styles (keyless)

`GET /styles/{id}.json` serves the compiled MapMap house styles with no
key and no metering. The two we publish are:

```
https://api.mapmap.ai/styles/mapmap-light-76ef8d@2.json
https://api.mapmap.ai/styles/mapmap-dark-762117@2.json
```

The `@2` suffix pins an immutable version, so a screenshot or a filmed
demo cannot change under you; drop it for "latest". Both point their
`territory` source at `pmtiles://https://tiles.mapmap.ai/uk.pmtiles`, read
over HTTP range requests.

These are the styles our own iOS and Android demo apps load, so they are
known to work under MapLibre Native. They are **not** a production
entitlement: `tiles.mapmap.ai` is our own CDN, free on our own surfaces,
and the metered tile API below is the supported path for an app you ship.

A detail worth knowing if you ever debug a 403 there: `tiles.mapmap.ai`
runs a browser-origin gate. A request declaring a foreign `Origin` or
`Referer` is refused; a request declaring neither, which is exactly what a
native MapLibre client sends, is served. That is the gate working as
designed, not an outage.

### The keyed tile API (production)

```
https://api.mapmap.ai/tiles/{territory}/style.json?api_key=snk_…
https://api.mapmap.ai/tiles/{territory}/tiles.json?api_key=snk_…
https://api.mapmap.ai/tiles/{territory}/{z}/{x}/{y}.mvt?api_key=snk_…
```

The query parameter is `api_key` (the `Authorization: Bearer` header form
works too, but a style document referenced by bare URL from a map client
cannot attach one, which is why the query form exists). Styles, sprites,
glyphs and tiles are all metered against that key. See
[Maps, tiles & Studio](/docs/maps) for territories, custom styles and
Studio.

### An installed territory (offline)

A signed territory package installed by `TerritoryStore` carries its own
PMTiles layer. `territoryStyle(territoryId:theme:)` returns a complete
style JSON whose sources are `pmtiles://<absolute local path>`, so the map
renders with the radios off and nothing is metered. That is the subject of
[Offline territories](#offline-territories) below.

## Keys in a native app

This is the part to read before you mint anything.

[Browser keys](/docs/api-reference#browser-keys) let you lock a key to the
websites it may be called from (`allowed_origins`) and to the map and
search surface (`scope: "maps"`). The first of those two properties
**breaks a native app**, and it fails closed rather than degrading.

An origin-restricted key requires the request to carry an `Origin` header,
or failing that a `Referer` the gateway can reduce to an origin. Those are
the two things a browser sends and a stolen key replayed from a server
cannot forge. A request presenting **neither** is refused, deliberately:

```json
{
  "type": "urn:sn-gateway:problem:origin-not-allowed",
  "title": "Origin not allowed",
  "status": 403,
  "detail": "this API key is restricted to browser origins and this request carried neither an Origin nor a Referer header; origin-restricted keys are for browsers — mint a key without allowed_origins for server-to-server use",
  "origin": null,
  "docs": "https://mapmap.ai/docs/api-reference#browser-keys",
  "hint": "browser keys are restricted to the origins named at issuance: mint one with POST /v1/keys {\"allowed_origins\": [\"https://app.example.com\"], \"scope\": \"maps\"}, or use a key with no allowed_origins for server-to-server calls"
}
```

(`origin` is `null` because the request presented none; the wording is the
gateway's own, quoted verbatim.)

MapLibre Native sends neither header. There is no `Origin` on a URLSession
or OkHttp request that no browser made, and adding one by hand would be a
string you typed rather than evidence of anything. So an origin-restricted
key in an iOS or Android app answers 403 on **every** tile, and you get a
blank basemap with no partial-success mode to mislead you.

**Mint a `maps`-scope key with no `allowed_origins`:**

```bash
curl -X POST "https://api.mapmap.ai/v1/keys" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "you@example.com",
    "accept_tos": true,
    "label": "ios-app",
    "scope": "maps"
  }'
```

`scope: "maps"` still does real work. It limits the key to tiles, style,
font and sprite reads, geocoding and its own status endpoints; every write
and every routing call answers 403 `scope-insufficient`. A key lifted out
of your app binary cannot order a matrix, publish a style or replace your
places dataset.

What it cannot do is stop the key being used from somewhere else, because
nothing about a native request identifies the app it came from. Treat a
key shipped in a binary as extractable and plan accordingly:

- **One key per app**, per platform, labelled, so you can revoke without
  touching anything else.
- **Always `scope: "maps"`.** Never ship a full-scope key in a binary.
- **Watch `GET /v1/usage`.** A key spending outside your release pattern
  is the signal you get.
- **Rotate by issue-then-revoke** on a schedule you set, shipped with an
  app update.
- If none of that is enough for your threat model, **proxy tiles through
  your own backend**, hold a server-side key there, and let your app
  authenticate to you instead. That is the only arrangement that actually
  binds map spend to your users, and it is the same conclusion every
  vendor in this category reaches.

Nothing above is specific to MapMap; a client-side map key is a shared
industry problem. The honest statement is that origin allow-lists solve it
for browsers and have no native equivalent.

## iOS (Swift)

MapLibre Native iOS ships as an official SwiftPM binary distribution. The
current release is **6.29.0** (23 August 2026).

```swift
// Package.swift
.package(
    url: "https://github.com/maplibre/maplibre-gl-native-distribution",
    from: "6.27.0"
)
```

In Xcode: **File > Add Package Dependencies**, that URL, and add the
`MapLibre` library to your app target. `from:` admits any 6.x, so this
resolves to the current release while holding a floor. Our own CarPlay demo
uses the same floor: 6.26.1 carries a fill-extrusion memory fix and 6.27.0
the PMTiles ambient cache, and a `pmtiles://` source is exactly what a
MapMap style uses.

CocoaPods works too: the pod is `MapLibre`, published in step with the
SwiftPM releases, deployment target iOS 12.0.

> **Anything using `MGL` prefixes or `import Mapbox` predates MapLibre
> Native 6.0.0.** The whole surface was renamed `MGL…` to `MLN…` in that
> release, along with the module name. Snippets you find elsewhere may not
> have caught up; the module is `MapLibre` and the view is `MLNMapView`.

A minimal map on a MapMap style:

```swift
import MapLibre
import UIKit

final class MapViewController: UIViewController, MLNMapViewDelegate {

    /// Metered production path. For evaluation, swap in the keyless house
    /// style: https://api.mapmap.ai/styles/mapmap-light-76ef8d@2.json
    private static let styleURL = URL(
        string: "https://api.mapmap.ai/tiles/uk/style.json?api_key=snk_…"
    )!

    private var mapView: MLNMapView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let map = MLNMapView(frame: view.bounds, styleURL: Self.styleURL)
        map.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        map.delegate = self

        // MapMap's canonical corner layout: attribution small bottom-left,
        // the MapMap mark bottom-right, never stacked. Turn MapLibre's own
        // ornaments off rather than letting them fight yours for the same
        // corners. If you draw neither, leave these visible: the OSM credit
        // is not optional (see "Attribution" below).
        map.logoView.isHidden = true
        map.attributionButton.isHidden = true
        map.compassView.isHidden = true

        map.setCenter(
            CLLocationCoordinate2D(latitude: 51.5074, longitude: -0.1278),
            zoomLevel: 12,
            animated: false
        )

        view.addSubview(map)
        mapView = map
    }

    // Called once the style and its sources are ready. Add your own
    // sources and layers here, never before.
    func mapView(_ mapView: MLNMapView, didFinishLoading style: MLNStyle) {
        // e.g. style.addSource(…) / style.addLayer(…)
    }
}
```

`MLNMapView` needs no explicit lifecycle wiring: it is a `UIView` and
manages itself. Two traps that have cost us real time:

- **`contentInset` gets overwritten.** `MLNMapView.automaticallyAdjustsContentInset`
  is already `false` by default, but MapLibre Native still runs the legacy
  adjustment, which consults the **view controller's** deprecated
  `automaticallyAdjustsScrollViewInsets` and rewrites `contentInset` with
  the safe-area insets on the next layout pass. If you set a focal inset
  for a chase camera, set `automaticallyAdjustsScrollViewInsets = false`
  on the controller. Re-applying the inset after layout does not work; it
  is overwritten again.
- **Nested `interpolate` on zoom.** A paint property whose expression
  nests a zoom `interpolate` inside another expression cannot be replaced
  with `setPaintProperty`-style updates and fails **silently**. Rebuild
  the layer instead of patching it.

### Keeping the key out of the style URL (iOS)

Putting `?api_key=` in the style URL is the supported path on iOS, and it
is the one we recommend. Header injection is not a documented contract
there: `MLNNetworkConfiguration.sharedManager.sessionConfiguration` exists
and can carry custom headers, but Apple explicitly tells you not to set
`Authorization` through `URLSessionConfiguration.httpAdditionalHeaders`,
and MapLibre's per-request hook (`MLNNetworkConfigurationDelegate`) is
marked as experimental and excluded from the generated documentation. Do
not build a release around it.

If you would rather not bake the key into a constant string, the
documented hook is `MLNOfflineStorageDelegate`, which lets you rewrite
every resource URL before it is fetched:

```swift
final class KeyInjector: NSObject, MLNOfflineStorageDelegate {
    func offlineStorage(
        _ storage: MLNOfflineStorage,
        urlForResourceOf kind: MLNResourceKind,
        with url: URL
    ) -> URL {
        guard url.host == "api.mapmap.ai" else { return url }
        var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
        components.queryItems = (components.queryItems ?? [])
            + [URLQueryItem(name: "api_key", value: Secrets.mapmapKey)]
        return components.url ?? url
    }
}

// Set once, before any MLNMapView exists.
MLNOfflineStorage.shared.delegate = KeyInjector()
```

The key still ships in the binary; this only keeps it out of the style URL
you hand around. Read [Keys in a native app](#keys-in-a-native-app) again
if you are relying on this for security rather than tidiness.

### SwiftUI

`MLNMapView` is UIKit. Wrap it once and reuse:

```swift
import MapLibre
import SwiftUI

struct MapMapView: UIViewRepresentable {
    let styleURL: URL

    func makeUIView(context: Context) -> MLNMapView {
        MLNMapView(frame: .zero, styleURL: styleURL)
    }

    func updateUIView(_ view: MLNMapView, context: Context) {
        if view.styleURL != styleURL { view.styleURL = styleURL }
    }
}
```

## Android (Kotlin)

MapLibre Native Android is on Maven Central as `org.maplibre.gl:android-sdk`.
`mavenCentral()` alone is enough; no custom repository is needed. Declare
it at the **root**, in `settings.gradle.kts`'s
`dependencyResolutionManagement`, not in a module:

```kotlin
dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
}
```

```kotlin
// app/build.gradle.kts
dependencies {
    implementation("org.maplibre.gl:android-sdk:13.4.1")
}
```

13.4.1 is the version our own Android modules build and test against; the
current release is 13.6.0. **Pin an exact version**, and note that
`android-sdk` changed renderer under the same coordinates: from **13.0.0**
it defaults to **Vulkan**, where 12.x was OpenGL ES. If you need the old
renderer, the artefact is `org.maplibre.gl:android-sdk-opengl`; there is
also `android-sdk-vulkan-opengl`, which carries both and picks at runtime
for a larger binary. Upgrading 12.x to 13.x without changing coordinates
changes your renderer, which is worth knowing before you chase a device
specific rendering bug.

MapLibre Native's own floor is `minSdk 23` and Java 11; `ai.mapmap:core`
needs `minSdk 26`, so an app using both sits at 26.

> **Anything importing `com.mapbox.mapboxsdk` predates MapLibre Native
> 11.0.0.** That release moved the package root to `org.maplibre.android`
> and renamed the classes with it (`MapboxMap` to `MapLibreMap`, `Mapbox`
> to `MapLibre`). The `org.maplibre.gl` group id is older than the package
> rename, so do not infer the import namespace from the coordinates.

**Permissions.** You usually do not need to declare any. The SDK's own
manifest merges in `INTERNET`, `ACCESS_NETWORK_STATE` and
`ACCESS_WIFI_STATE`, and also `ACCESS_COARSE_LOCATION` and
`ACCESS_FINE_LOCATION` whether or not you use the location component. If
your app has no business asking for location, strip them explicitly rather
than shipping a permission you cannot justify at review:

```xml
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
    tools:node="remove" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
    tools:node="remove" />
```

`MapLibre.getInstance(context)` must run once before any `MapView` is
constructed or inflated (`MapView` throws
`MapLibreConfigurationException` otherwise), and `MapView` needs its
lifecycle callbacks forwarded by hand: there is no `LifecycleObserver` in
the SDK. Missing either is the usual cause of a black rectangle:

```kotlin
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.maplibre.android.MapLibre
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.Style

class MapActivity : AppCompatActivity() {

    private lateinit var mapView: MapView

    override fun onCreate(savedInstanceState: Bundle?) {
        // Before setContentView, and before any MapView exists.
        MapLibre.getInstance(this)
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_map)

        mapView = findViewById(R.id.mapView)
        mapView.onCreate(savedInstanceState)
        mapView.getMapAsync { map -> onMapReady(map) }
    }

    private fun onMapReady(map: MapLibreMap) {
        // Metered production path. For evaluation, swap in the keyless
        // house style: https://api.mapmap.ai/styles/mapmap-light-76ef8d@2.json
        val styleUri = "https://api.mapmap.ai/tiles/uk/style.json?api_key=snk_…"

        map.setStyle(Style.Builder().fromUri(styleUri)) { style ->
            // Style and sources are ready. Add your own layers here.
        }

        map.cameraPosition = CameraPosition.Builder()
            .target(LatLng(51.5074, -0.1278))
            .zoom(12.0)
            .build()
    }

    // MapView holds a GL surface and will leak or render black without
    // every one of these.
    override fun onStart() { super.onStart(); mapView.onStart() }
    override fun onResume() { super.onResume(); mapView.onResume() }
    override fun onPause() { mapView.onPause(); super.onPause() }
    override fun onStop() { mapView.onStop(); super.onStop() }
    override fun onLowMemory() { super.onLowMemory(); mapView.onLowMemory() }
    override fun onDestroy() { mapView.onDestroy(); super.onDestroy() }
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        mapView.onSaveInstanceState(outState)
    }
}
```

```xml
<!-- res/layout/activity_map.xml -->
<org.maplibre.android.maps.MapView
    android:id="@+id/mapView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
```

### Jetpack Compose

There is no first-party Compose map, so host the `View`. Because
`MapView`'s lifecycle methods are not optional, forward them from a
`DisposableEffect` rather than relying on `AndroidView`'s `onRelease`
alone:

```kotlin
@Composable
fun MapMapMap(styleUri: String, modifier: Modifier = Modifier) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val mapView = remember {
        MapLibre.getInstance(context)
        MapView(context).apply { onCreate(null) }
    }

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_START -> mapView.onStart()
                Lifecycle.Event.ON_RESUME -> mapView.onResume()
                Lifecycle.Event.ON_PAUSE -> mapView.onPause()
                Lifecycle.Event.ON_STOP -> mapView.onStop()
                // ON_DESTROY is deliberately absent: onDispose below owns
                // it, and destroying twice crashes.
                else -> Unit
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
            mapView.onDestroy()
        }
    }

    AndroidView(modifier = modifier, factory = { mapView })

    LaunchedEffect(styleUri) {
        mapView.getMapAsync { map -> map.setStyle(Style.Builder().fromUri(styleUri)) }
    }
}
```

(`Style.Builder().fromUri` is the current name; `fromUrl` is deprecated
but still present, which is why old snippets still compile with a
warning.)

There is also an official
[`maplibre-compose`](https://github.com/maplibre/maplibre-compose)
Compose Multiplatform stack. It is a parallel implementation rather than a
Compose skin over the View SDK, it is beta on both Android and iOS, and its
own release notes warn that minor releases can carry breaking changes. We
have not built on it; the `AndroidView` host above is the conservative
route.

### Keeping the key out of the style URL (Android)

Unlike iOS, Android has a reliable header hook. `HttpRequestUtil` swaps in
your own OkHttp `Call.Factory` for every map request, and the
configuration survives across `MapView` instances:

```kotlin
import okhttp3.OkHttpClient
import org.maplibre.android.module.http.HttpRequestUtil

val client = OkHttpClient.Builder()
    .addInterceptor { chain ->
        val request = chain.request()
        if (request.url.host == "api.mapmap.ai") {
            chain.proceed(
                request.newBuilder()
                    .header("Authorization", "Bearer ${BuildConfig.MAPMAP_KEY}")
                    .build(),
            )
        } else {
            chain.proceed(request)
        }
    }
    .build()

// Once, before any MapView exists. Pass null to reset to the default.
HttpRequestUtil.setOkHttpClient(client)
```

The gateway accepts the bearer form on every endpoint, so this works. It
is tidier than a key in a URL, and it is **not** a security control: see
[Keys in a native app](#keys-in-a-native-app).

## React Native

[`@mapmap/react-native`](/docs/sdks) is an Expo module wrapping the two
native SDKs. Like them, it exposes no map component: its TypeScript
surface is the navigation handle, typed guidance events, a pure-TS mock
that runs in Expo Go, and a config plugin.

Render with
[`@maplibre/maplibre-react-native`](https://github.com/maplibre/maplibre-react-native)
and feed it the style the module returns:

```ts
import { getMapmapNav } from "@mapmap/react-native";

// false for the native module, true for the pure-TS mock (runs in Expo Go).
const nav = getMapmapNav(false);
const styleJson = await nav.getTerritoryStyle("uk", "light");
```

That is the offline path (local `pmtiles://` sources). For an online map,
pass one of the style URLs above to the renderer directly. We soak-tested
`@maplibre/maplibre-react-native` under Expo with 3D buildings on, which is
the memory-hungriest case: cost was bounded and it did not run out of
memory, so it is a reasonable choice rather than merely the only one.

The architecture, and what the React Native bridge does and does not do
(including the parts that are compile-verified rather than device-tested),
is in [MapMap Nav](/docs/nav).

## Offline territories

The reason to use MapMap on mobile at all is usually that the map has to
work with no network. A signed territory package carries the PMTiles
basemap, the routing graph and a geocode index together, so the basemap
and the route come from the same install and cannot disagree.

The shape of it:

1. Install a signed `.snpkg` package with `TerritoryStore` (ed25519
   manifest, BLAKE3 content-addressed layers, differential over-the-air
   updates with an atomic swap).
2. Ask for its style: `territoryStyle(territoryId:theme:)` on iOS,
   `territoryStyle(territoryId, theme)` on Android,
   `getTerritoryStyle(territoryId, theme)` in React Native. You get a
   complete style JSON whose sources are `pmtiles://<absolute path>`.
3. Load that JSON string into MapLibre Native: `MLNMapView.styleJSON` on
   iOS, `Style.Builder().fromJson(styleJson)` on Android.

Nothing in step 3 touches the network, and nothing is metered.
`layerPath(territoryId:kind:)` gives you the raw PMTiles file if you would
rather assemble your own style around it.

Full detail, including the trust model, the OTA differential mechanism,
the hosted channel's verifying key and how to publish your own channel:

- [Territory packages & OTA updates](/docs/territories)
- [Trust model](/docs/territories#trust-model)
- [Differential over-the-air updates](/docs/territories#differential-over-the-air-updates)
- [Fetching territories over the API](/docs/territories#fetching-territories-over-the-api)
- [The hosted channel's verifying key](/docs/territories#the-hosted-channels-verifying-key)

**Do not use MapLibre Native's own offline pack API for a MapMap
basemap.** `MLNOfflineStorage` and Android's `OfflineManager` predownload
a tile pyramid from a tile server into MapLibre's own cache. A territory
package already is the offline basemap, versioned, signed and updated
differentially, and running both means two caches, two update paths and
two answers to "what does this device have". Use MapLibre's offline packs
only for sources you own that MapMap does not package.

## Attribution

The map data is OpenStreetMap on the OpenMapTiles schema. The credit is a
licence obligation (ODbL and CC-BY), it follows the data rather than the
platform, and it applies identically to a phone screen, a car display and
a web page. The exact string, shared verbatim by the web SDK and both
demo apps:

```
© OpenStreetMap contributors © OpenMapTiles
```

Either leave MapLibre's own attribution control visible, or hide it and
draw the credit yourself. MapMap's canonical layout is the attribution
small at the **bottom-left** and the MapMap mark at the **bottom-right**,
never stacked in the same corner.

## What we do not ship

Stated plainly, so you can plan against what exists rather than what a
roadmap implies.

**No mobile map rendering SDK.** There is no MapMap map view for iOS or
Android, and no wrapper around MapLibre Native comparable to
`@mapmap/maps` on the web. `ai.mapmap:core` declares no MapLibre
dependency at all. You add MapLibre Native, you own the view, the camera
and the styling, and the SDK hands you style JSON. That is a deliberate
choice rather than a gap we are about to close: a map view is the part of
a navigation app teams most want to control, and wrapping it well is a
larger commitment than wrapping it badly.

**No native turn-by-turn navigation UI.** The SDKs give you guidance
*state*: manoeuvres, distances, lane hints and formatted strings through
`NavigationEngine` and `GuidanceDisplay`, plus voice. The HUD, the
manoeuvre card, the route line, the puck and the chase camera are yours to
build. There is no drop-in navigation view controller or Compose
navigation screen.

**What you can reuse today:**

- **The React Native app.** [`@mapmap/react-native`](/docs/sdks) is
  published and real: the navigation handle, typed guidance and territory
  events, a mock that runs in Expo Go without a native build, and an Expo
  config plugin that writes the location usage strings and the background
  modes. [MapMap Nav](/docs/nav) documents the reference architecture and
  the early-access Android app you can install and drive today.
- **The CarPlay and Android Auto modules.** Android's `:car` and
  `:car-maplibre`, and iOS's `MapMapCarPlay`, wrap the platform car
  surfaces around MapMap guidance. `:car-maplibre` in particular contains
  a working chase camera, route line and puck over MapLibre Native, which
  is the closest thing in the tree to a reference map implementation. They
  are **beta and source-distribution only** today: no published artefact,
  built from a source checkout, public artefacts planned for `sdk-v0.7.0`
  after head-unit hardware validation. Read
  [CarPlay & Android Auto](/docs/car) before you commit to a date, because
  Apple's entitlement and Google's Play review are the long poles, not the
  code.

**Version skew you will notice.** The Android artefact
(`ai.mapmap:core`) and the iOS package (`MapMapKit`) are not on the same
version number and are not released in lockstep. The current pair is in
the compatibility table on [SDKs & installation](/docs/sdks); trust that
table over any README.

## Next steps

- [SDKs & installation](/docs/sdks) for install coordinates, the signing-key
  trust anchor and licensing.
- [Android API reference](/docs/android) for every public class of
  `ai.mapmap:core`.
- [Territory packages & OTA updates](/docs/territories) for the offline
  basemap.
- [Maps, tiles & Studio](/docs/maps) for territories, custom styles and the
  Studio editor.
- [Browser keys](/docs/api-reference#browser-keys) for the browser-side
  version of the key question above.
