50,000 free calls a month, card-free. Get an API key →

Documentation menu
docs / maps · raw .md
Studio tour: design, test, publish · 3:03 · all videos

Maps: vector tiles, styles & Studio

MapMap is a maps platform as well as a routing one: the gateway serves vector tiles, TileJSON and ready-to-render MapLibre styles straight from the same territory data the routing engine uses. Style them in Studio, or let an agent style them through the MCP style tools.

Status, honestly: the hosted gateway at https://api.mapmap.ai is live: sign up for a key, or run the self-host distro; every call below works identically against your own deployment. These endpoints are enabled per deployment: they answer 404 until the operator stages the tile and asset directories (the hosted gateway has them enabled, with territories on every continent; see territories for coverage).

Set up your shell once so every snippet on this page is copy-pasteable:

sh
export BASE=https://api.mapmap.ai   # or your self-host gateway
export API_KEY=snk_…                # issued by POST /v1/keys; see the quickstart

Metered endpoints accept the key as Authorization: Bearer $API_KEY (preferred) or as an ?api_key=snk_… query parameter; the query form exists for URL-only contexts like a style URL handed to MapLibre. No key yet? The quickstart issues one in a single curl; base URL, auth and the error envelope are defined in full on the conventions page.

Searching and interacting with POIs on a live MapMap map · © OpenStreetMap contributors © OpenMapTiles

Vector tiles

MethodPathWhat it returns
GET/tiles/{territory}/{z}/{x}/{y}.mvtOne Mapbox Vector Tile (.pbf also accepted), gzip-encoded, strong ETag, immutable caching. In-range tile with no data is 204
GET/tiles/{territory}/tiles.jsonTileJSON 3.0: zoom range, bounds and centre from the territory archive, OSM attribution
GET/tiles/{territory}/style.jsonA ready-to-render MapLibre GL style over the territory's vector source

All three take your normal API key and are metered per request at the Standard price class (see pricing). They are not charged against the offline download allowance, which applies only to bulk territory-package downloads.

Territory slugs (uk below) are listed in the territories guide; on a self-hosted deployment GET /territories reports what you have staged.

sh
curl -fsS "$BASE/tiles/uk/tiles.json" -H "Authorization: Bearer $API_KEY"
json
{
  "tilejson": "3.0.0",
  "name": "uk",
  "scheme": "xyz",
  "tiles": ["https://api.mapmap.ai/tiles/uk/{z}/{x}/{y}.mvt?v=1720981132"],
  "minzoom": 0,
  "maxzoom": 14,
  "bounds": [-8.65, 49.86, 1.77, 60.86],
  "center": [-2.0, 54.0, 6.0],
  "attribution": "© OpenStreetMap contributors"
}

(The ?v= discriminator busts caches atomically when the operator republishes a territory archive; keep it in the URLs you pass through.)

Render a map in one move: point MapLibre GL at the style URL. This example is complete; note the container div must have an explicit height, or the map renders zero pixels tall and the page looks silently blank (the classic MapLibre gotcha):

html
<!-- In production, pin an exact maplibre-gl version and add SRI
     (integrity="sha384-…" crossorigin="anonymous"), or bundle it via npm. -->
<link href="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.js"></script>

<div id="map" style="height: 100vh"></div>

<script>
  const map = new maplibregl.Map({
    container: "map",
    style: "https://api.mapmap.ai/tiles/uk/style.json?api_key=snk_…",
    center: [-1.5, 52.6], // lon, lat
    zoom: 6,
  });
</script>

Territory base maps include ocean and land at all zooms: the map factory stages the global sources (OSM ocean water polygons, Natural Earth, lake centrelines) into every territory build.

Server-side raster/PNG rendering is not offered; clients render the vector tiles themselves.

Hosted styles (the style API)

Styles are versioned and immutable: publishing writes a new version, and a versioned style URL can be cached forever.

MethodPathAuthWhat it does
GET/stylesnoneList hosted styles (id, name, latest version, style URL)
GET/styles/{id}.jsonnoneThe latest compiled MapLibre style (short TTL)
GET/styles/{id}@{version}.jsonnoneOne immutable compiled version (cached forever)
GET/styles/{id}/themenoneThe editable theme document behind the latest version (served no-store)
POST/styleskeyCreate a style as version 1. Metered, Standard class
POST/styles/{id}keyPublish the next immutable version. Metered, Standard class

Reads are public and unmetered, so compiled styles can be referenced by bare URL from browser map clients. The hosted gateway ships two house styles, MapMap Light and MapMap Dark, listed by GET /styles; every style you publish appears alongside them under your own id.

Create a style: POST /styles

FieldRequiredDefaultDescription
nameyesHuman-readable name, 1–120 characters; its kebab-case slug seeds the style id
themenothe default theme carrying nameA full theme document
sh
curl -fsS -X POST "$BASE/styles" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Midnight Fleet",
    "theme": {
      "name": "midnight-fleet",
      "base": "dark",
      "palette": { "water": "#0b2038" }
    }
  }'

201 Created:

json
{
  "id": "midnight-fleet-9f3a2c",
  "version": 1,
  "style_url": "https://api.mapmap.ai/styles/midnight-fleet-9f3a2c@1.json",
  "theme": { "name": "midnight-fleet", "base": "dark", "palette": { "water": "#0b2038" } }
}

The id is the name's slug plus six hex characters, so same-named styles never collide. style_url is the immutable compiled style: drop it straight into a MapLibre client.

Publish a new version: POST /styles/{id}

The body is not the theme itself: it is a wrapper with the full theme document under "theme". POSTing a bare theme is a 400.

FieldRequiredDefaultDescription
themeyesThe complete theme document to publish as the next version (a full document, not a diff)

Read-modify-write: fetch the current document from GET /styles/{id}/theme (served no-store, so you never publish from a stale base), edit it, and POST it back:

sh
curl -fsS -X POST "$BASE/styles/midnight-fleet-9f3a2c" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "theme": {
      "name": "midnight-fleet",
      "base": "dark",
      "palette": { "water": "#0d2742" }
    }
  }'

200 OK:

json
{
  "id": "midnight-fleet-9f3a2c",
  "version": 2,
  "style_url": "https://api.mapmap.ai/styles/midnight-fleet-9f3a2c@2.json"
}

Existing versions are never modified; GET /styles/{id}.json starts serving the new version within its 60-second TTL.

Errors

An invalid theme is a 422 (application/problem+json) whose problems list names the offending slot or layer and the accepted values, so agents can self-correct and resubmit:

json
{
  "type": "urn:sn-gateway:problem:invalid-theme",
  "title": "Theme validation failed",
  "status": 422,
  "detail": "theme validation failed: unknown palette slot \"watr\"; accepted slots: background, water, waterway, …",
  "problems": [
    "unknown palette slot \"watr\"; accepted slots: background, water, waterway, landcover, landuse, park, building, aeroway, road, roadMajor, path, rail, boundary, boundaryMinor, textPrimary, textSecondary, textHalo"
  ]
}
StatusType slugWhen
400bad-requestMalformed JSON body, or a name that is empty or over 120 characters
401unauthorizedMissing, malformed or revoked API key on a metered endpoint
404not-foundUnknown territory or style id, or tiles/styles/assets not staged on this deployment
422invalid-themeTheme failed validation; see the problems list
429quota-exceeded / rate-limitedMonthly quota exhausted or per-minute limit hit

The full error envelope, quota behaviour and the 402-vs-429 distinction are on the conventions page.

The theme document

A theme is a small JSON document the theme engine compiles into a full MapLibre style-spec v8 style.json. The same engine runs in Studio, in the gateway and in signed territory packages, so a theme renders identically everywhere.

json
{
  "name": "midnight-fleet",
  "base": "dark",
  "palette": { "water": "#0b2038", "roadMajor": "#8a6d3b" },
  "layers": {
    "building": { "visible": false },
    "road-minor": { "paint": { "line-width": 2 }, "minzoom": 10 }
  }
}
  • base is light or dark; the 19 palette slots below recolour the whole map. Colours are #rgb/#rrggbb/#rrggbbaa or rgb()/rgba()/hsl()/hsla().
  • Per-layer overrides: visible, paint/layout (per-key merge), filter, minzoom/maxzoom.
  • fonts.regular sets the label fontstack (default "MapMap Sans Regular"); keep it on a bundled fontstack or labels drop.
  • fonts.labels optionally retypesets label types individually: { "country", "place", "road", "water", "poi", "housenumber" }, each a fontstack string; absent kinds inherit fonts.regular. Example: "fonts": { "regular": "MapMap Sans Regular", "labels": { "place": "MapMap Sans Bold", "water": "MapMap Sans Italic" } }. Publishing a fontstack this server does not host is a 422 naming the hosted stacks.
  • glyphs and sprite optionally override the compiled style's glyph and sprite URLs.
  • extra_layers: extra fully-formed MapLibre layers inserted above the base map, below labels. Each must use the territory source and one of the source-layers the tiles emit: aerodrome_label, aeroway, boundary, building, housenumber, landcover, landuse, mountain_peak, park, place, poi, transportation, transportation_name, water, water_name, waterway.
  • extra: an opaque block the compiler ignores (capped at 256 KB serialised); Studio stores its navigation design and POI theming here.
  • buildings_3d: true renders buildings as 3D extrusions from OSM height data; see 3D buildings for where to (and not to) use it.
  • Attribution is structural: every compiled style carries "© OpenStreetMap contributors © OpenMapTiles" on its tile source, and validation rejects any theme that tries to drop it.

Palette slots

All 19 slots, with the light/dark base defaults. roadMotorway inherits whatever roadMajor resolves to until you set it explicitly, so recolouring roadMajor alone moves motorways too; set both for the classic orange-motorways-over-yellow-primaries look:

SlotLightDark
background#f4f2ee#12161c
water#a8c8e8#1b2c40
waterway#a8c8e8#1b2c40
landcover#e3e8dd#182029
landuse#ece8e1#171e26
park#c8e2bf#1a2a1f
ice#d9e3ec#151e29
building#e0d6c4#252d37
aeroway#dcd9d2#232b34
road#ffffff#2b333d
roadMajor#f6d9a0#4a5461
roadMotorwayfollows roadMajorfollows roadMajor
path#d9d4ca#2a323c
rail#d0cbc2#262e37
boundary#b9a6c9#4a3d57
boundaryMinor#c9bcd6#3a3247
textPrimary#3a3a3a#d6dbe1
textSecondary#6b6b6b#9aa3ad
textHalo#ffffff#12161c

Styleable layer ids

Per-layer overrides target these 29 skeleton layer ids (paint order, first = bottom): background, landcover, landuse, park, water, waterway, aeroway, building, building-outline, rail, pedestrian-areas, road-path, road-minor-casing, road-major-casing, road-minor, road-major, road-bridge-casing, road-bridge, boundary-minor, boundary, then road-oneway and the label layers housenumber, road-labels, water-name, poi-labels, mountain-peak-labels, aerodrome-labels, place-labels, country-labels.

landuse and landcover carry a match on the feature class so hospitals, schools, cemeteries, military land, retail, grass and wood read distinctly; the tints are blended from existing palette slots, and an unrecognised class falls through to the plain slot colour. Roads tagged brunnel: tunnel are dimmed in place, while brunnel: bridge roads are redrawn by road-bridge-casing/road-bridge above the flat network so an overpass reads as crossing over what it spans. road-oneway draws one-way direction arrows from z16 using a pinned Noto Sans Regular glyph, so it needs no sprite.

road-minor-casing and road-major-casing draw the darker edge under each road. Both casings paint beneath both road fills, so a minor road's casing never crosses a major road at a junction. Their colours are derived from the road/roadMajor slots blended toward textSecondary rather than being separate palette slots, so overriding road restyles its casing too. building-outline is derived from building the same way.

Agents get the same catalogue programmatically from the list_style_layers MCP tool; an unknown id in a theme is a 422 echoing this list.

3D buildings

Setting "buildings_3d": true in a theme (or the Studio toggle under Layers → Buildings) adds a building-3d fill-extrusion layer: from zoom 15 the flat footprints rise into volumes using the OpenStreetMap render_height/render_min_height attributes already in MapMap tiles. City centres carry real surveyed heights; elsewhere buildings fall back to a uniform default, so the effect is strongest where it matters most. Tilt the map (pitch) to see it.

On the web you can also toggle it at runtime without recompiling the style:

ts
const map = createMap({ container: "map", style: "light" });
map.setBuildings3d(true); // and false to return to flat footprints

Mobile: off by default, and never in navigation views. MapLibre Native (the iOS/Android renderer) currently has prohibitive fill-extrusion memory use at street-level zooms (maplibre-native#4107; 900 MB+ at zoom 17, which iOS answers by killing the app). The native SDKs ship two pure JSON transforms: styleWithBuildings3d(styleJson, enabled) to opt a map browsing view in on hardware you've measured, and styleForNavigation(styleJson) which strips 3D and should wrap any style you load into a turn-by-turn view. The default will flip once MapLibre Native lands level-of-detail culling for extrusions.

3D buildings and the globe projection on a live MapMap map · © OpenStreetMap contributors © OpenMapTiles

Studio

Studio is the browser editor over the same theme engine, in four tabs: Map design (pick the base, recolour the palette slots, toggle layers and adjust widths and opacities per layer, colour the POI categories, with a live map preview), Navigation (below), Markers (place coloured glyph pins, dots or small custom images with labels, saved with the style so the SDKs draw them), and Publish. Your working theme autosaves in the browser.

The Map design tab's preset picker offers first-party starting points: the plain Light and Dark bases, plus Calm Cab Day and Calm Cab Night: low-distraction in-cab themes with muted landcover, dimmed water, a prominent road hierarchy and POI label noise switched off, tuned so the default route line stays the highest-contrast element on screen. A preset loads into the editor as an ordinary theme document (your navigation design is kept), so everything remains editable and publishable as your own style.

From the Publish tab's "Use this style" panel: paste your snk_ key and publish: the first publish creates the hosted style (POST /styles), and each one after publishes the next immutable version (POST /styles/{id}). Drop the returned style URL into @mapmap/maps or raw MapLibre, or download the compiled style.json instead. Agents get the same powers through the MCP style tools (list_style_layers, get_style, create_style, set_palette, set_layer_paint); see the MCP guide.

Designing a theme live in Studio, then publishing it through the theme compiler · © OpenStreetMap contributors © OpenMapTiles

Studio's Navigation panel designs the turn-by-turn look (route line, current-position puck and banner instruction), previewed live on the map. The result is a small design-token block stored under extra.nav in the theme JSON Studio downloads and copies:

json
{
  "extra": {
    "nav": {
      "version": 1,
      "route":  { "color": "#ff7a1f", "width": 4.5, "opacity": 1, "casingColor": "#8a3d00" },
      "puck":   { "color": "#1f6fff", "size": 14, "headingArrow": true,
                  "imageUrl": "data:image/png;base64,…" },
      "banner": { "background": "#101418", "textColor": "#ffffff", "fontSize": 16, "showLanes": true,
                  "padding": 10, "cornerRadius": 10, "maxWidth": 340 },
      "camera": { "pitch": 60, "zoom": 17.5, "speedMps": 12 },
      "alerts": {
        "background": "#101418", "textColor": "#ffffff", "outlineColor": "#ffffff",
        "cornerRadius": 10, "outlineWidth": 1.5,
        "escalatedBackground": "#c0392b", "escalatedTextColor": "#ffffff",
        "iconSet": "european", "mapIconSize": 1, "mapMinZoom": 12,
        "alertSound": "cameraCalm", "escalatedSound": "cameraUrgent",
        "leadDistance": "standard", "chipPosition": "aboveSpeed",
        "corridorColor": "#e05c6c", "corridorOpacity": 0.55,
        "kinds": {
          "fixed": { "showOnMap": true, "showOnMapWhileNavigating": true,
                     "alertWhileDriving": true, "showInRoutePreview": true,
                     "alertMode": "always", "audioMode": "earcon" }
        }
      }
    }
  }
}
  • route: line colour, width (px at mid zoom), opacity and casing colour; the defaults are the web SDK RouteLayer look.

  • puck: current-position colour, diameter in px, and whether the heading arrow is drawn. imageUrl (optional since v1) sets a custom puck image: an https: or data: URI (PNG/SVG) that replaces the built-in dot and arrow; size still scales it, and it rotates to the heading.

  • banner: background/text colours, primary text size in px, and whether the lane diagram row is shown, matching the web SDK GuidanceBanner conventions. padding (px, 6–24, default 10), cornerRadius (px, 0–24, default 10), maxWidth (px, 200–520, default 340) and height (px, 40–120, unset = auto content height) are optional since v1; the block stays "version": 1 and themes saved without them load with the defaults.

  • alerts (optional since v1): the safety-camera alert design, written by Studio's "Safety alerts" section. Chip tokens are background, textColor, outlineColor, cornerRadius (px, 0–24) and outlineWidth (px, 0–4); escalatedBackground and escalatedTextColor style the over-the-limit state. iconSet is one of european, us, minimal or brand, with optional per-kind iconUrlOverrides (https: or data: URIs only). alertSound and escalatedSound pick the two earcons from the built-in library (cameraCalm, cameraUrgent, overspeedSoft, zoneEnter, zoneClear), each with an optional alertSoundUrl / escalatedSoundUrl override under the same https: or data: rule. mapIconSize (0.75–1.5 of the default pin ramp) and mapMinZoom (9–14) control the map pins, corridorColor and corridorOpacity (0–1) tint the average-speed corridor on the route line, leadDistance is one of short, standard or long (150 m, 250 m and 400 m floors), and chipPosition is one of aboveSpeed, topLeading or topTrailing. kinds carries one entry per camera kind (fixed, average, red_light, mobile_site, unknown), each with showOnMap, showOnMapWhileNavigating, alertWhileDriving and showInRoutePreview booleans, an alertMode of always, whenSpeeding or never, and an audioMode of off, earcon or earcon_and_speech. Themes saved without the block load unchanged and stay "version": 1; a block that is present always parses complete, escalated tokens included.

    showOnMap and showOnMapWhileNavigating are separate on purpose: browsing the map and driving it are different decisions, and the market warns with or without a planned route, so map display is not gated behind active guidance. A theme written before the split carries only showOnMap, and its navigating value reads as true.

    There are two sounds, not ten. The calm earcon and its escalated variant are the whole audible vocabulary: which camera it is travels in the icon and in speech, and how urgent it is travels in the timbre, so a driver is never asked to learn one chime per camera kind. Set a kind's audioMode to off if you want it silent. The average-speed corridor keeps its own entry and exit pair, which marks a boundary rather than a point alert.

    The built-in earcons are served from https://mapmap.ai/earcons/ (earcon-camera-calm.mp3, earcon-camera-urgent.mp3, earcon-overspeed-soft.mp3, earcon-zone-enter.mp3, earcon-zone-clear.mp3). They are public and unmetered; self-host them by passing your own base URL to navAlertSoundUrl.

    Two behaviours are deliberate rather than configurable. The visual chip is unconditional and calm while audio is the escalation channel, so a driver within the limit still sees the alert and only hears it when alertMode says so. And the chip position is a fixed set of slots rather than free coordinates, so an alert can never cover the speed or the limit, which are the two numbers it is asking the driver to act on.

    Jurisdiction policy is server side and is not styleable. Whether camera positions may be returned at all is decided per country by the gateway before any camera leaves it. This block designs how a permitted alert looks; it cannot widen what is permitted, and a theme carrying an alert design still shows nothing in a territory whose policy omits cameras.

  • camera (optional since v1, whole block and each field): the drive camera Studio's "Drive the route" demo uses and the web SDK's NavigationCamera reads as its defaults. pitch (degrees, 0–85, default 60), zoom (14–20, default 17.5) and speedMps (drive speed in metres per second, 2–40, default 12). Explicit NavigationCamera options still win: option > design > built-in. Themes saved without the block load unchanged.

The block never enters the compiled style.json (the compiler ignores it entirely), but it does travel with the theme document, hosted ones included: Studio's publish sends extra with the theme (whenever the design differs from the defaults), the gateway stores it verbatim with that version, capped at 256 KB serialised, and serves it back from GET /styles/{id}/theme. So teams consuming a style via the API get the navigation design too. Pass a downloaded theme file to the web SDK's createMap, which parses extra.nav into map.navDesign and applies it across the navigation UI:

ts
import { createMap, RouteLayer, GuidanceBanner, PositionPuck } from "@mapmap/maps";
import theme from "./midnight-fleet.theme.json"; // downloaded from Studio

const map = createMap({ container: "map", apiKey: "snk_…", style: theme });
await map.whenReady();

const routes = new RouteLayer(map);            // route line + casing from extra.nav.route
const banner = new GuidanceBanner(document.body, map.navDesign?.banner);
const puck = new PositionPuck(map);            // dot + arrow or custom image
puck.setLocation({ lat: 51.5074, lon: -0.1278 }, 45);

…or fetch the design straight from a hosted style's theme endpoint (public, uncached, always the latest publish):

ts
import { navDesignFromThemeUrl } from "@mapmap/maps";

const design = await navDesignFromThemeUrl(
  "https://api.mapmap.ai/styles/midnight-fleet-a1b2c3/theme",
);

Parsing is lenient and clamped exactly as Studio previews it; without a nav block everything keeps the SDK's built-in look. See SDKs for the full reference.

POI theming

The POIs panel (Map design tab) colours the eight POI categories the MapMap maps group OpenMapTiles poi classes into: food_drink, shopping, transport, lodging, health, culture_leisure, education and services (the fallback for any class not grouped elsewhere). Per category you can recolour the dot drawn under the label and the label text; both preview live. The result is a block under extra.poi, alongside the navigation design:

json
{
  "extra": {
    "poi": {
      "version": 1,
      "categories": {
        "food_drink": { "color": "#ff6b35", "textColor": "#b34700" },
        "transport":  { "color": "#0aa8a8" }
      }
    }
  }
}

Both fields are optional per category: an absent color keeps the built-in dot colour, an absent textColor keeps the theme's textSecondary label colour. Studio omits the whole block while everything is at the defaults, so existing themes are untouched.

Like the navigation design, the block never enters the compiled style.json: it travels with the theme document, publishes with it, and comes back from GET /styles/{id}/theme. The MapMap website maps (Studio's preview included) apply it to their category dots and label text automatically. In the web SDK, a theme document passed to createMap parses into map.poiDesign, and the label-text part applies to any live map:

ts
import { createMap, applyPoiDesign, poiDesignFromThemeUrl } from "@mapmap/maps";
import theme from "./midnight-fleet.theme.json"; // downloaded from Studio

const map = createMap({ container: "map", style: theme });
await map.whenReady();
if (map.poiDesign) applyPoiDesign(map.map, map.poiDesign);

// …or from a hosted style's theme endpoint:
const design = await poiDesignFromThemeUrl(
  "https://api.mapmap.ai/styles/midnight-fleet-a1b2c3/theme",
);

The SDK's default styles label POIs as text (no dots), so applyPoiDesign covers the text colours; the coloured dot icons are drawn by the MapMap website maps today, and packaged sprite icons are planned as part of marker packs. Parsing is lenient: unknown categories and invalid colours are dropped, never fatal.

Label languages

Tiles carry OpenStreetMap name:* translations, 70+ languages on populated tiles, alongside each place's local name. The web SDK's buildStyle takes a labelLanguage option: an ISO code ("de", "ja", "ar", …) renders labels in that language with local-name fallback, "local" shows every place in its own language, and the default is English-first. Pass the built style to createMap:

ts
import { createMap, buildStyle } from "@mapmap/maps";

const map = createMap({
  container: "map",
  apiKey: "snk_…",
  style: buildStyle({ theme: "dark", labelLanguage: "ja" }),
});

See it live on the map: the language picker offers English, local names and fifteen more. Every script renders: the bundled Noto fontstacks are the OpenMapTiles merged builds, covering Arabic, Hebrew, Thai, Myanmar, Khmer, the Indic scripts, CJK and more. Arabic and Hebrew shaping needs MapLibre's RTL text plugin: call maplibregl.setRTLTextPlugin(…) once in your app, as the live map does.

Fonts and sprites

Compiled styles reference glyphs and sprites served by the gateway (public, unmetered, immutable caching):

  • GET /fonts/{fontstack}/{range}.pbf: SDF glyph ranges for the bundled fontstacks: MapMap Sans Regular (plus Bold and Italic, all three composited with the merged Noto stacks so every script renders), Noto Sans Regular (plus Bold and Italic), Barlow Regular, Fira Sans Regular, IBM Plex Sans Regular, Inter Regular, Lato Regular, Montserrat Regular, Noto Serif Regular, Nunito Regular, Open Sans Regular, Rubik Regular, Source Sans 3 Regular and Work Sans Regular, all SIL Open Font Licence, static instances. Keep every fontstack the theme names on one of these or those labels drop. Families added in a release reach the hosted fonts endpoint with the next assets deploy; Studio previews them immediately.
  • GET /sprite/sprite[@2x].json|png: the first-party sprite sheet (marker, dot, arrow, lane diagrams, road shields, tunnel-restricted).

Status, honestly: compiled styles default their glyph URL to https://fonts.mapmap.ai/{fontstack}/{range}.pbf, which is live and serving. On self-hosted deployments, set the theme's glyphs field to your own gateway's route, e.g. $BASE/fonts/{fontstack}/{range}.pbf. Using your own hand-written style instead of a compiled theme? Point its glyphs and sprite fields at these same routes.

On self-hosted deployments these routes serve when SN_MAP_ASSETS_DIR is set; tiles need SN_TILES_DIR and hosted styles need SN_STYLES_DIR (see self-hosting).

Web SDK quickstart

Availability: @mapmap/maps is live on npm.

@mapmap/maps wraps MapLibre GL with the MapMap style, the pmtiles protocol and typed routing helpers:

sh
npm install @mapmap/maps maplibre-gl pmtiles

As with raw MapLibre, the page needs a container element with an explicit height, <div id="map" style="height: 100vh"></div>, or the map renders blank.

ts
import { createMap, RouteLayer } from "@mapmap/maps";
import "maplibre-gl/dist/maplibre-gl.css";

const map = createMap({
  container: "map",
  apiKey: "snk_…",
  style: "light", // "light" | "dark" | a Studio theme | a style URL
  center: [-1.5, 52.6], // lon, lat
  zoom: 6,
});

await map.whenReady();

const routes = new RouteLayer(map);
await routes.route(
  { lng: -0.1278, lat: 51.5074 },
  { lng: -1.5106, lat: 52.4081 },
  { profile: "truck", truck: { heightM: 4.0, weightT: 40, hazmat: true, tunnelCode: "C" } },
);

createMap is the entry point used throughout these docs and by Studio's publish snippet. It is a functional alias for the MapMapMap class, so new MapMapMap({ … }) takes the same options if you prefer the constructor form.

The truck options cover ADR (the European agreement on carriage of dangerous goods by road; tunnel codes B–E restrict which tunnels a hazmat load may use); the full tunnel-code table is on the conventions page. The style option accepts a Studio theme document directly, so a style built in Studio drops straight into your app. Full SDK reference: SDKs.

Exporting the map as an image (design-tool plugins, PNG downloads, server-side thumbnails)? Two gotchas: MapLibre 5 moved preserveDrawingBuffer into canvasContextAttributes, and captures should render offscreen at the target size rather than screenshot the interactive map. The full recipe is in the @mapmap/maps README ("Map screenshots / static image export").

Landmark directions

Turn instructions on the map can reference a recognisable place instead of a bare street name: "Turn right just after KFC". The landmarks come from MapMap's first-party POI index, queried around each manoeuvre point on the selected route.

The feature is confidence-gated, because a wrong landmark is worse than none. A place only replaces the street name when it clears a salience bar (petrol stations, supermarkets, pubs and fast food score highest, and a known brand raises the score), sits within 40 m of the junction, and shows no signs of staleness: places whose records carry closed or disused markers are never referenced. When no candidate clears the bar, the instruction keeps the street name.

Toggle it with "Landmark directions" under Options in the directions panel; the choice is remembered on your device. The same phrasing engine will feed spoken guidance: voice support is coming to the SDKs.

Report a map issue

Spotted a map error: a road that doesn't exist, a vehicle restriction that's wrong, a shop that closed years ago? Use the flag button on the map: click the map to pin the exact spot, pick a category, optionally add a note and an email for follow-up, and submit. API users can file the same report directly with POST /map-issues (one Standard-class call; the request body is {lat, lon, category, note?, contact?}; see /openapi.json on your gateway for the accepted categories).

Every report is picked up by an agent, which investigates it against OpenStreetMap ground truth, our own geocoder and satellite imagery, and publishes its diagnosis on the public Map Health board. Reports are answered and triaged in minutes, and many fixes go live the same hour.

Reports never edit the map directly, and neither does the agent. A human-verified fix ships in two directions: immediately, as a data override on our own routing gateways, and upstream as a proper OpenStreetMap contribution, so your report improves the map for everyone, not just MapMap users.

Attribution

Map data derives from OpenStreetMap. Anything you render or republish must credit "© OpenStreetMap contributors" with a link to openstreetmap.org/copyright; the compiled styles carry this for you.

Next steps

  • Conventions: base URL, auth, error envelope, quotas and ADR tunnel codes
  • Territories: which territories exist and what they cover
  • SDKs: the full @mapmap/maps reference
  • MCP guide: style the map from an agent
Restyle a map in 60 seconds · 1:09 · all videos