The Mapbox Vector Tile Spec & Tile Geometry Jump to heading
Every confusing thing about vector tiles traces back to one design decision: geometry is stored as small integers on a grid that belongs to the tile, not as coordinates on the Earth. Understanding that grid — how positions are encoded onto it, what happens at its edges, and what precision it actually offers — explains the blocky coastlines, the seams, the duplicated roads and the surprising file sizes all at once.
The Problem This Topic Solves Jump to heading
You are writing or debugging something that produces or consumes vector tiles, and the behaviour does not match a mental model built on GeoJSON. Coordinates come back as small integers. A polygon that was valid in the source is reported as invalid by a tile reader. A feature you know exists appears twice. A line that should be continuous shows a hairline gap at a tile boundary.
The failure scenario worth naming is the silent one: a pipeline that encodes geometry without accounting for the grid produces tiles that render acceptably at the zoom the developer tested and fall apart elsewhere — self-intersections at low zoom where rounding collapsed two vertices onto the same grid point, invisible slivers at high zoom, and polygons whose winding order flipped so a client renders them as holes.
Prerequisites Jump to heading
Understand projection basics from Coordinate Reference Systems in OSM, because the tile grid sits on top of Web Mercator and inherits its distortions. Know the parent section’s tile pyramid model from OSM Vector Tiles & Rendering Pipelines. And a working knowledge of protocol buffers helps, since MVT is a protobuf schema — the same encoding dissected in PBF File Structure Deep Dive.
The Grid and Its Precision Jump to heading
A tile’s extent declares the size of its local coordinate grid; 4096 is conventional and nearly universal. Coordinates run from 0 to extent across the tile, with the origin at the top-left corner.
The ground distance represented by one grid unit is therefore the tile’s ground width divided by the extent. At the equator a zoom-\(z\) tile spans
so one grid unit at extent 4096 covers
At zoom 14 that is about 0.6 metres; at zoom 8, about 38 metres; at zoom 4, about 600 metres. Away from the equator the figure shrinks by a factor of \(\cos(\varphi)\), so the same grid is finer at high latitudes.
Two practical rules fall out. Do not simplify below the grid: a tolerance finer than \(u(z)\) produces vertices that round onto the same grid point, costing time and, worse, sometimes creating zero-length segments that make a polygon invalid. And expect quantisation artefacts at low zoom: they are the format working as designed, not a bug in your simplifier.
Command and Parameter Encoding Jump to heading
Geometry inside a feature is a flat array of unsigned integers holding interleaved commands and parameters.
A command integer packs an identifier in its low three bits and a repeat count in the remaining bits: command_integer = (id & 0x7) | (count << 3). Three commands exist — MoveTo (1), LineTo (2) and ClosePath (7). Each MoveTo or LineTo is followed by 2 × count parameter integers; ClosePath takes none.
Parameters are relative to the previous point and zigzag-encoded so small negative deltas stay small: parameter = (value << 1) ^ (value >> 31). Relative encoding is what keeps a detailed coastline compact, since consecutive vertices differ by a handful of grid units even when the absolute position is large.
Winding, Validity and Polygons Jump to heading
Polygon rings in MVT version 2 carry meaning in their winding order: an exterior ring is clockwise in the tile’s screen coordinate system, and an interior ring — a hole — is counter-clockwise. A polygon feature’s geometry is a sequence of rings where each exterior ring begins a new polygon and the counter-clockwise rings following it are its holes.
This is a frequent source of bugs for anyone coming from the OGC world, where the convention is the opposite way round and where hole membership is structural rather than inferred from winding. Encoding a ring with the wrong orientation produces a polygon a client renders as a hole in nothing, which typically shows up as a mysteriously missing landmass.
Validity is also affected by the grid. A ring that is valid in source coordinates can become invalid after quantisation, because two nearly-coincident vertices round to the same grid point and produce a zero-length segment or a spike. Encoders should therefore validate after quantisation, not before — the detection techniques in Detecting Self-Intersecting OSM Polygons with Shapely apply directly, just on the rounded coordinates.
Clipping and the Buffer Jump to heading
Geometry that extends beyond the tile must be clipped, and clipping exactly at the boundary is almost never what you want. A road drawn eight pixels wide needs geometry to continue past the edge, or the client has nothing to draw into the outer four pixels and a hairline seam appears.
The buffer is expressed in grid units and geometry is clipped to [-buffer, extent + buffer]. Coordinates outside 0…extent are legal in the format precisely for this reason. Larger buffers cost bytes in every tile; too-small buffers cost visible seams. The trade-off, and how to pick a value from your styled line widths, is in Choosing Tile Extent and Buffer Values, and the debugging workflow when it goes wrong is in Debugging Features Clipped at Tile Edges.
Validation and Error-Handling Matrix Jump to heading
| Condition | Root cause | Detection | Remediation |
|---|---|---|---|
| Geometry scattered across the tile | Cursor reset between rings | Rings start at the tile origin | Keep one cursor for the whole feature |
| Landmass renders as a hole | Exterior ring wound the wrong way | Ring is counter-clockwise where clockwise expected | Enforce winding after quantisation |
| Polygon invalid only in tiles | Vertices collapsed by rounding | Validity fails on grid coordinates, passes on source | Simplify to at least one grid unit first |
| Hairline seams at tile edges | Buffer smaller than half the styled width | Gaps along boundaries at specific zooms | Increase the buffer for the widest styled layer |
| Coordinates outside 0…extent | Buffer geometry, not corruption | Values slightly negative or above extent | Expected; clamp only when rendering, never when encoding |
| Enormous tiles at high zoom | Deltas large because points are unordered | Parameter values far from zero | Order vertices along the geometry before encoding |
| Attributes missing on some features | Key or value index out of range | Decoder error naming the index | Build the key and value tables before encoding features |
Performance and Scale Jump to heading
The encoding’s efficiency depends almost entirely on delta size, and delta size depends on vertex ordering. Geometry traversed in spatial order produces deltas of a few grid units, which zigzag and varint encoding store in one byte. The same vertices in arbitrary order produce deltas spanning the tile, costing two or three bytes each and defeating the design.
Attributes are the other half of the budget. Keys and values live in per-layer tables and features reference them by index, so a repeated string costs one entry plus a small index per feature rather than a full copy. That makes distinct value count the number to watch: a layer with ten distinct values across a million features is cheap, and one with a million distinct names is not.
Finally, tile size scales with feature count and vertex count, not with geographic area. A dense city tile at zoom 14 can be fifty times the size of a rural tile at the same zoom, which is why size budgets must be enforced per tile rather than assumed from the zoom.
Failure Modes and Gotchas Jump to heading
- The cursor is per feature, not per ring. Resetting it between rings is the most common decoder bug and produces geometry scattered from the origin.
- Winding is opposite to the OGC convention. Exterior rings are clockwise in screen coordinates here, and hole membership is inferred from orientation rather than declared.
- Validate after quantisation. A polygon valid in source coordinates can be invalid on the grid.
- Coordinates outside the extent are normal. They are buffer geometry; clamping them at encode time is what creates seams.
- Extent is per layer. Nothing requires every layer in a tile to share one extent, and a decoder that assumes 4096 will misplace geometry in a layer that chose otherwise.
- A feature identifier is not unique in the tile set. The same road carries the same identifier in every tile it crosses.
- Zigzag is not two’s complement. Decoding parameters as signed integers directly produces enormous wrong values rather than an obvious error.
Integration Points Jump to heading
Upstream, the geometry entering an encoder should already be projected, simplified for the target zoom and valid — the generalization decisions in Cartographic Generalization of OSM Data happen before encoding, not during it. Downstream, the encoded tiles are packaged and served as described in Serving & Invalidating OSM Tiles.
When you are writing the encoder yourself rather than using a generator, Encoding OSM Geometry into MVT with Python puts the whole model into working code.
Guides in This Topic Jump to heading
- Encoding OSM Geometry into MVT with Python — projection, quantisation, command encoding and attribute tables in working code.
- Choosing Tile Extent and Buffer Values — deriving both numbers from styled line widths and a size budget.
- Debugging Features Clipped at Tile Edges — isolating whether a seam comes from the buffer, the style or the source geometry.
Frequently Asked Questions Jump to heading
Why are vector tile coordinates integers rather than degrees?
Because integers on a small grid compress dramatically better and decode faster. A tile-local grid of a few thousand units means most coordinate deltas fit in a single byte after zigzag and variable-length encoding, where geographic degrees would need eight bytes each. The cost is that precision is fixed by the grid and therefore by the zoom level, which is exactly the trade a transport format should make.
What does a coordinate outside the tile extent mean?
It is buffer geometry, and it is entirely normal. Features are clipped to the tile plus a margin so that a client drawing a thick line has geometry to draw into the outer pixels rather than leaving a seam. Coordinates slightly below zero or slightly above the extent are the format working as intended; clamping them during encoding is what produces the hairline gaps people then try to fix by increasing the buffer.
Why did my polygon become invalid only after encoding?
Because quantisation to the tile grid can collapse two nearly-coincident vertices onto the same point, producing a zero-length segment or a spike that makes the ring invalid. The source geometry was fine at full precision. The fix is to simplify with a tolerance of at least one grid unit before encoding, and to run validity checks on the quantised coordinates rather than on the originals.
Which way should polygon rings wind?
Exterior rings clockwise and interior rings counter-clockwise, in the tile’s screen coordinate system where the origin is top-left and y increases downwards. This is the opposite of the convention most geospatial libraries use, and hole membership is inferred from the orientation rather than declared structurally. An exterior ring wound the wrong way is typically rendered as a hole, which looks like a missing feature rather than an orientation bug.
Can different layers in one tile use different extents?
Yes. The extent is declared per layer, so nothing prevents a detailed layer from using a finer grid than a coarse one. In practice almost everything uses the conventional value, which is why decoders that hard-code it usually work — right up until they encounter a tile that does not, and silently misplace every coordinate in that layer by the ratio between the assumed and actual extents.
Related Jump to heading
- OSM Vector Tiles & Rendering Pipelines — the parent section and the pipeline this encoding sits inside.
- Cartographic Generalization of OSM Data — the simplification that must precede quantisation.
- Coordinate Reference Systems in OSM — the projection the tile grid is built on.
- PBF File Structure Deep Dive — the same protobuf and delta-encoding techniques in OSM’s own format.
- Detecting Self-Intersecting OSM Polygons with Shapely — validity checking, applied to quantised coordinates.
- Spatial Index Selection: R-tree vs H3 vs Quadkey — the quadtree addressing tiles share.
Up one level: OSM Vector Tiles & Rendering Pipelines.