A composite serves the vector tiles of several sources through a single endpoint. Requesting a composite tile fetches each member concurrently and merges their layers into one MVT — ideal for stacking a basemap with an overlay without re-tiling either one.
Two ways to compose
Ad-hoc — join ids with +
No configuration needed. Join source ids with + in the URL:
# Merge the `roads` and `water` sources for one tile
GET /data/roads+water/14/8716/5683.pbf
# Composite TileJSON
GET /data/roads+water.json
Named — [[composites]] config
Give a reusable alias to a fixed set of members:
[[composites]]
id = "world-base"
sources = ["protomaps", "zurich"]
GET /data/world-base/14/8716/5683.pbf
GET /data/world-base.json
A configured [[composites]] id always wins over ad-hoc + parsing.
How merging works
- Concurrent fetch — members are fetched in parallel.
- Decompression — gzip members are decompressed before merging.
- MLT — MLT members are transcoded to MVT first.
- Layer merge — layers with the same name have their features concatenated; the dictionary keys/values are remapped into a shared index space (equal keys and values are deduplicated). Distinct layer names are preserved.
- Missing members — a member that has no tile at the requested coordinate is
skipped. If every member misses, an empty MVT is returned with
200 OK(not404). - Raster members — rejected with
400; composites are vector-only.
Composite TileJSON
GET /data/{id}.json returns a merged TileJSON:
vector_layers— the union of every member's layers, deduped by layer id (first occurrence wins), empty ids dropped.minzoom— the maximum of the members' minzooms.maxzoom— the minimum of the members' maxzooms.
The zoom range is intersected so the composite only advertises zooms that all members cover. Requests outside the intersection naturally return an empty tile.
Errors
| Situation | Response |
|---|---|
| A member id does not resolve to a loaded source | 404 |
| A member is a raster source | 400 |
| All members miss the tile | 200 with an empty MVT |