PMTiles

PMTiles is a single-file archive format for an entire pyramid of map tiles (raster or vector). One .pmtiles file on static hosting — S3, R2, GCS, or any HTTP server that honours Range requests — is a complete basemap or overlay backend. No tile server, no Honua server.

MapLibre GL JS renders PMTiles through a registered protocol handler (maplibregl.addProtocol("pmtiles", …)). The Honua SDK wires that up for you: the runtime auto-registers the pmtiles:// protocol on map attach, and the pmtiles package is an optional peer dependency imported lazily, so a build that never touches PMTiles pays no bundle cost.

Discovering a direct archive with connect()

The shared connect() workflow owns direct PMTiles discovery. An explicit protocol: "pmtiles" works with an ordinary HTTPS asset URL. For auto, use a pmtiles://https://... marker; a .pmtiles filename is deliberately not enough evidence and causes an ambiguous-protocol error before cache, authentication, or network work begins.

import { connect } from "@honua/sdk-js";

const connection = await connect({
  endpoint: "pmtiles://https://example.com/basemaps/hawaii.pmtiles",
  protocol: "auto",
  authorizationScopeFingerprint: "public",
});

const source = connection.source();
console.log(source.descriptor.locator.sourceType); // "vector" or "raster"
console.log(connection.inspection.sources[0]?.metadata?.pmtiles?.bounds);

Discovery runs through the normal client auth/interceptor/timeout pipeline and accepts only exact HTTP 206 ranges. It permits at most two physical HTTP attempts, 512 KiB per range, 1 MiB reserved in total, and 4 MiB of cumulative inflated internal directory/metadata output; caller-provided pmtiles.limits may only lower those ceilings. A retryable or authentication response is not replayed inside this bounded lane. Redirects, compressed HTTP ranges, aggregate whole-archive coverage, unbounded internal compression, inconsistent Content-Range/validators, unknown magic, cancellation, and response overflow fail closed. Disjoint ranges require one strong ETag or canonical Last-Modified validator; weak ETags are not byte-identity evidence. The cache identity binds the canonical asset URL, authorization-scope digest, and normalized transfer/decompression policy; cached inspection retains and cross-binds retrieval time, PMTiles spec version, validator evidence, and the complete bounded range ledger plus cumulative decompressed-byte count. Cache replay rechecks that count against the current caller ceiling. The snapshot retains at most 1,000,000 UTF-8 bytes of canonical raw metadata and at most 2,048 raw vector_layers entries, using the same admission rules on live discovery and cache replay. Normalized vector-layer metadata is capped at 8,000 retained structural nodes so every accepted live result remains portable through the shared cache envelope. Cached extents, the exact capability and provenance record, and derived source fields are cross-bound to the reviewed archive metadata. The returned typed PMTiles adapter reuses that reviewed description and does not perform a second native fetch.

Rendering a PMTiles source (runtime)

loadMapPackage auto-registers the pmtiles:// protocol before it applies the composed style, so a pmtiles source binding renders with no manual addProtocol call. The registration is lazy (the pmtiles and maplibre-gl packages are imported only when a PMTiles-backed map loads) and idempotent across every map you attach.

import { HonuaClient } from "@honua/sdk-js/honua";
import { HONUA_MAP_PACKAGE_FORMAT_V1, loadMapPackage } from "@honua/sdk-js/runtime";
import * as maplibregl from "maplibre-gl";

const map = new maplibregl.Map({ container: "map", style: { version: 8, sources: {}, layers: [] } });
await new Promise((resolve) => map.on("load", resolve));

await loadMapPackage(
  {
    format: HONUA_MAP_PACKAGE_FORMAT_V1,
    mapPackageId: "pmtiles-basemap",
    sourceBindings: [
      {
        sourceId: "basemap",
        protocol: "pmtiles",
        // `sourceType` picks the MapLibre source kind: "vector" (default) or "raster".
        locator: { url: "pmtiles://https://example.com/basemap.pmtiles", sourceType: "vector" },
      },
    ],
    mapSpec: {
      version: 8,
      sources: {},
      layers: [
        { id: "water", type: "fill", source: "basemap", "source-layer": "water", paint: { "fill-color": "#9cf" } },
      ],
    },
  },
  map,
  // No protocol-backed sources → no Honua server needed. `HonuaClient` is
  // required by the contract but never called here.
  { client: new HonuaClient({ baseUrl: location.origin }), skipCompatibilityCheck: true },
);

A pmtiles source binding projects onto a MapLibre-native source ({ type: "vector" | "raster", url: "pmtiles://…" }) via projectSourceBindings. The locator.sourceType hint selects the MapLibre source kind and defaults to vector (the common PMTiles basemap case).

Registering the protocol yourself

If you add a PMTiles source to a live map imperatively (runtime.addSource, which is synchronous) or drive MapLibre directly, register the protocol first:

import { ensurePmtilesProtocol } from "@honua/sdk-js/runtime";

await ensurePmtilesProtocol(); // lazy, idempotent — safe to call repeatedly
map.addSource("basemap", { type: "vector", url: "pmtiles://https://example.com/basemap.pmtiles" });

Inspecting archive metadata (contract)

PMTiles participates in the protocol-neutral Dataset / Source model as a tiles-only protocol. Its default capability set is { tiles }, so the canonical query family throws HonuaCapabilityNotSupportedError — an archive has no feature-query surface. Archive metadata is inspected through the typed escape hatch or the standalone helper:

import { createDataset, describePmtilesArchive } from "@honua/sdk-js/contract";

// Standalone: inspect any archive URL.
const info = await describePmtilesArchive("https://example.com/basemap.pmtiles");
console.log(info.tileKind); // "mvt" | "png" | "jpeg" | "webp" | "avif" | "unknown"
console.log(info.bounds); // [west, south, east, north] in degrees
console.log(info.minZoom, info.maxZoom);
console.log(info.vectorLayers.map((layer) => layer.id)); // source-layer names

// Through a Dataset: `describe()` on the typed adapter handle.
const dataset = createDataset({
  id: "basemaps",
  client,
  skipCompatibilityCheck: true,
  sources: [
    {
      id: "basemap",
      protocol: "pmtiles",
      locator: { url: "pmtiles://https://example.com/basemap.pmtiles" },
      capabilities: new Set(["tiles"]),
    },
  ],
});
const archive = dataset.source("basemap")!.protocol("pmtiles");
const meta = await archive!.describe();

PmtilesArchiveDescription carries url, tileKind, bounds, minZoom, maxZoom, center, vectorLayers, an optional attribution, and the raw metadata JSON.

Build-less CDN recipe

PMTiles works from a plain HTML page with no bundler — load maplibre-gl and pmtiles from a CDN and register the protocol:

<script src="https://unpkg.com/pmtiles@4/dist/pmtiles.js"></script>
<link href="https://unpkg.com/maplibre-gl@6/dist/maplibre-gl.css" rel="stylesheet" />
<div id="map" style="position:absolute;inset:0"></div>
<script type="module">
  import * as maplibregl from "https://unpkg.com/maplibre-gl@6/dist/maplibre-gl.mjs";

  const protocol = new pmtiles.Protocol();
  maplibregl.addProtocol("pmtiles", protocol.tile);
  new maplibregl.Map({
    container: "map",
    style: {
      version: 8,
      sources: { basemap: { type: "raster", url: "pmtiles://https://example.com/basemap.pmtiles" } },
      layers: [{ id: "basemap", type: "raster", source: "basemap" }],
    },
  });
</script>

Notes