fgb-vt reads any FlatGeobuf 3.x file. How a file is written decides how fast tiles come out of it.
ogr2ogr -f FlatGeobuf out.fgb input.gpkg -t_srs EPSG:4326 -nln parcels
GDAL writes a spatial index by default and stores features in index (Hilbert) order, which is what range reads need: the features of a tile sit close together in the file. Keep the index: without it, every tile scans the whole file (fgb-vt refuses beyond maxScanBytes, 32 MiB).
The index node size (GDAL's default 16) rarely needs changing. Larger nodes make the tree shallower (fewer index round trips) but each node covers a larger area, so more index bytes are read per tile.
Write WGS 84 longitude/latitude (EPSG:4326) or Web Mercator (EPSG:3857); both are read natively. Geographic CRSs on modern datums within about 2 m of WGS 84 (NAD83, ETRS89, GDA94, GDA2020, NZGD2000, SIRGAS 2000, JGD2000/2011) are read as longitude/latitude too. Any other CRS needs a projection on every source (crs: { toWgs84, fromWgs84 }), which costs a conversion per vertex and per tile; crs: 'lonlat' accepts the offset of another geographic datum instead.
At low zooms a tile covers much of the dataset, and every feature in it has to be read, whatever is drawn in the end. Per-tile budgets (maxReadBytes, maxFeatures) stop such tiles; pointGrid thins points in the output but not the reading.
Serve low zooms from a smaller file instead. Sources sharing a layer name are merged into one layer, so the switch is invisible to clients:
sources: [
{ name: 'buildings', path: 'buildings-overview.fgb', options: { maxZoom: 11 } },
{ name: 'buildings', path: 'buildings.fgb', options: { minZoom: 12 } },
]
Overviews can be made with GDAL, for example:
# Polygons and lines: simplify (tolerance in degrees)
ogr2ogr -f FlatGeobuf buildings-overview.fgb buildings.fgb -simplify 0.0005
# Points: keep a sample
ogr2ogr -f FlatGeobuf poi-overview.fgb poi.fgb -dialect sqlite -sql "SELECT * FROM poi WHERE abs(random()) % 20 = 0"
id, fid, gid or ogc_fid becomes the MVT feature id, which renderers need for feature state (hover, selection). Name another column with idColumn.properties.The flatgeobuf Rust library also stores features in index order. The JavaScript library (flatgeobuf on npm, tested with 4.6) writes no spatial index at all, and every number as a Double column (so there is no integer id column for feature ids): fine for small files, otherwise rewrite them with GDAL.
gogama/flatgeobuf (Go), v1.0.0: do not use FileWriter.IndexData. Its index does not match the data it writes:
parts, so multipolygons and collections stored as parts get empty boxes and can never be found.fgb-vt detects such files and raises IndexMismatchError (useIndex: false scans them instead, within maxScanBytes), and fgb-vt check reports every affected index entry. To write valid files with gogama, build the index yourself and write it with FileWriter.Index, then the features in the same order with FileWriter.Data:
parts;packedrtree.HilbertSort;4 + size for each feature (the size prefix included);packedrtree.New(refs, nodeSize).test/fixtures/writers/gogama/main.go (buildIndex) does exactly this.
npx @geozelot/fgb-vt check out.fgb
It reads the whole file once: the header and CRS, every spatial index node (valid boxes, parents containing their children, leaf offsets in increasing order), and every feature (decoded, at the offset its index entry says, inside the entry's box). Tiling reads only what tiles need, so these defects otherwise show only when a tile reaches them. The exit code is 1 if the file has errors; --json prints a machine-readable report, e.g. for a check in the pipeline that writes the files. In code, checkFile(connector, path) returns the same report.
The report also gives the node overlap: the total area of the index nodes above the leaves relative to the dataset extent. Values near 1 mean compact, spatially sorted nodes; large values mean tiles read many features they do not need.