GeoParquet
A GeoParquet (.parquet or .geoparquet) file stores features as a columnar
table, the format GeoPandas, DuckDB and GDAL write. maplibre-yaml reads it into
an ordinary GeoJSON source, and reads only the columns the map uses, so a
table with dozens of columns downloads only the few you show. Popups,
classify:, clustering and fitTo work as for a .geojson file. It is one
of the large-data formats; for which one suits
your data’s size, see Choose a format for large data.
Quick start
Section titled “Quick start”Keep type: geojson and point url: at the file:
type: mapid: geoparquet-wellsconfig: center: [10.2, 42.15] zoom: 10 mapStyle: "https://tiles.openfreemap.org/styles/positron" fitTo: { source: wells, padding: 20 }sources: wells: type: geojson url: /gallery-assets/cloud-native/points.parquetlayers: - id: wells type: circle source: wells paint: circle-radius: 4 circle-color: ["match", ["get", "kind"], "well", "#e63946", "station", "#457b9d", "#2a9d8f"] interactive: click: popup: - h3: [{ property: kind }] - p: [{ str: "Value: " }, { property: value }]The file is on this documentation site; to run the document on yours,
download points.parquet
into gallery-assets/cloud-native/. You should see red and blue points;
click one for its kind and value. The gallery’s
GeoParquet source shows it live.
Which columns are read
Section titled “Which columns are read”maplibre-yaml reads the geometry plus the properties the document uses for
that source: here kind and value, not the rest of the file. It finds them
in ["get", …] and ["has", …] in paint, layout and filter, {name}
tokens in text, popup and legend property: items, classify: field and
normalize, the source’s promoteId, filter and clusterProperties, and a
story’s setFilter, setPaintProperty and setLayoutProperty actions.
To choose yourself, set columns::
sources: wells: type: geojson url: data/wells.parquet columns: [kind, value, owner] # or: columns: allA clicked feature (ml-map:layer-click, queryRenderedFeatures) carries
only the columns that were read. If your page’s own JavaScript reads other
properties, list them in columns:, or write columns: all.
Whole file or by view
Section titled “Whole file or by view”By default a file is read whole when it is small and by view when it is
large. load: changes that:
load: | What it reads | Needs |
|---|---|---|
none, or all (default) | Small files whole (the columns the map uses, from every row). Large ones by view (see Large files), when the file and server allow it; otherwise the map says why and how to fix it | Any web server for a small file |
viewport | Only the rows in view, read again as the map moves; nothing below minzoom (default 10), and nothing for a view holding more than maxFeatures features (default 150,000) | A bbox column, rows sorted by location, and a server with range requests |
Reading by view works because a GeoParquet file stores rows in groups, and
its footer records each group’s extent from the bbox column; groups outside
the view are never downloaded. That only pays off when nearby rows sit in the
same groups, in a file sorted by location. A file whose groups each span most
of the area is read whole instead, with a console note; one without a bbox
column is refused with a notice that says how to add it. With load: viewport,
fitTo frames the extent recorded in the file’s metadata, at the outermost
zoom whose view is read (never below minzoom nor over maxFeatures), so the
map opens with features drawn. classify: with method: works too: the
breaks come from one column of every row, read once when the source loads
(Class breaks on large data).
Large files
Section titled “Large files”The limits
Section titled “The limits”A whole read costs memory and time in the browser, and compressed bytes understate it, so everything is decided from the file’s footer before any data is downloaded:
- Read whole up to 400,000 rows, 50 MB of the columns the map reads (compressed), and 100 MB once those columns are decoded (NYC’s citywide parcels decode to 142 MB and took over 4 GB to draw). Between 10 and 50 MB a console note says it may take a few seconds. Brooklyn’s 275,628 parcels read whole in 13–17 s on a desktop and pan at 42–50 fps.
- Over any of those, the file is read by view when it has a
bboxcolumn and sorted row groups and the server supports range requests, with a note for the author. Otherwise it is refused, and the notice names what stops it being read by view and how to fix that. - By view, a view isn’t read when it would hold more than
maxFeaturesfeatures (default 150,000), estimated from the footer: each row group’s rows times the share of the group’sbboxextent that the view covers. Nor is it read when the row groups it touches hold more than 3 ×maxFeaturesrows: a row group is decoded whole, so a file in 100,000-row groups decoded about 600,000 rows for a view of 145,000 features (3.8 GB, 10 fps). Readers see “Too many features to show here — zoom in to see them”, also belowminzoom, until they zoom in.
A server without range requests (python -m http.server) sends the whole file
at once: fine for a small file, refused before download for one over 50 MB.
Make it draw
Section titled “Make it draw”Each note and notice names the way that applies; here they all are.
-
Say
load: viewport. A large file is read by view anyway; saying so drops the note. -
Raise
maxFeatures. It is the one override: the most features one read takes, by view, and above 400,000 it raises the whole-file limit too. It costs memory, so the notes suggest it only up to 600,000 features loaded whole, or 250,000 in a view, and say how much; the 100 MB decoded limit still applies.sources:lots:type: geojsonurl: data/lots.parquetload: { maxFeatures: 600000 } # a 550,000-feature file, read whole -
Make the file readable by view: a
bboxcolumn, rows sorted along a Hilbert curve, about 10,000 rows per row group. With geoparquet-io, nothing to install but uv:Terminal window uvx --from geoparquet-io==1.7.0 gpio sort hilbert lots.parquet lots-hilbert.parquet \--row-group-size 10000 --add-bbox# from a FlatGeobuf, GeoJSON or GeoPackage file (sorted the same way)uvx --from geoparquet-io==1.7.0 gpio convert geoparquet lots.fgb lots.parquet --row-group-size 10000GDAL’s
ogr2ogr -f Parquet -lco WRITE_COVERING_BBOX=YES -lco SORT_BY_BBOX=YES -lco ROW_GROUP_SIZE=10000works too, but packs row groups less tightly: on NYC’s parcels a borough view touched 1.5–2 times the rows of a Hilbert sort, which can be enough to keep a view unread. -
For a citywide view, convert it to PMTiles. PMTiles draws at every zoom (1–3 s and 58–60 fps on a desktop for NYC’s 856,687 parcels). The PMTiles guide has the one-line tylertoo command.
-
Or write overview levels into the file, so a wide view reads a simplified level instead of asking to zoom in (Overview files, experimental).
npx maplibre-yaml@latest prepare lots.parquet reads the file’s footer and
prints the step that applies, with the YAML to paste
(Choose a format for large data).
Overview files (experimental)
Section titled “Overview files (experimental)”An overview file carries its own zoom pyramid: tylertoo’s overview writes a
simplified copy of the data per zoom level as extra rows of the same file,
listed in its geo:overviews key. Read by view, the map reads the level
that fits the zoom, with no YAML change:
uv run --with tylertoo==0.8.0 python -c "import tylertoo as t; t.overview('lots.parquet', 'lots-ov.parquet', min_zoom=0, max_zoom=14, collapse_square=True)"sources: lots: type: geojson url: lots-ov.parquet load: viewport # the overview level that fits the zoom (experimental)- Which level. At each zoom the map reads the finest level built for that
zoom or a coarser one: at zoom 11.4, the zoom-11 level, or zoom 10’s when
there is none. Below the coarsest level it reads the coarsest. A level that
declares no zoom gets one from its ground resolution (
gsd). minzoom. By view, a plain file reads nothing below zoom 10. An overview file with nominzoomof yours reads its coarsest level at every wider zoom instead; aminzoomyou write still wins.- The budget applies to the level read. A view of that level holding
more than
maxFeatures(default 150,000) isn’t read, and readers see “Too many features to show here — zoom in”, with an author hint about the level. The map doesn’t fall back to a coarser level. - Whole reads use full detail. A file small enough to read whole is read
at its full-detail level, and so are
classify:breaks,mlym emitandmlym breaks. Each feature is drawn once, never once per level. - Popups and hover work at every level: each row of each level is a real
feature with all its columns, and
promoteIdworks within a level. When the zoom crosses to another level, the map redraws from that level and resets hover and other feature state for the source’s layers. A popup opened at a coarse level stays as it was until you close it. load: { mode: viewport, overviews: false }reads only the full-detail level, by view: for debugging, or for a file whose coarse levels look wrong.- Versions. The
geo:overviewsspec is a draft (0.2.0). A file declaring another major version is read at its full-detail level, with an author hint.
What the coarse levels look like is the producer’s choice. With tylertoo
0.8.0 on a parcel fabric they drop most small lots, or, with
collapse_square=True, draw them as squares that fill a citywide view past
the budget; Overview GeoParquet has
the measurements, and why PMTiles stays the route for parcels and building
footprints seen citywide.
Make a GeoParquet file
Section titled “Make a GeoParquet file”geoparquet-io (gpio; no GDAL needed): uvx --from geoparquet-io==1.7.0 gpio convert geoparquet in.geojson out.parquet --row-group-size 10000 writes a
Hilbert-sorted file with a bbox column, ready for load: viewport.
GDAL (ogr2ogr, also inside QGIS). Writing Parquet needs GDAL built with
its Arrow/Parquet driver (3.9 or later for the bbox options): many distro
packages leave it out. conda-forge’s gdal with libgdal-arrow-parquet, or
the ghcr.io/osgeo/gdal Docker image, have it; ogrinfo --formats | grep Parquet tells.
ogr2ogr -f Parquet out.parquet in.geojson -lco COMPRESSION=SNAPPY# for load: viewport: a bbox column, rows sorted by location, smaller row groups# (a Hilbert sort, above, packs them tighter)ogr2ogr -f Parquet out.parquet in.geojson -lco WRITE_COVERING_BBOX=YES \ -lco SORT_BY_BBOX=YES -lco ROW_GROUP_SIZE=10000Add -t_srs EPSG:4326 when the source data is in a projected CRS.
GeoPandas (Python):
import geopandas as gpdgdf = gpd.read_file("in.geojson").to_crs(4326)gdf.to_parquet("out.parquet") # WKB geometry, snappy# for load: viewport: sort by location, then write the bbox columngdf = gdf.iloc[gdf.geometry.hilbert_distance().argsort()]gdf.to_parquet("out.parquet", write_covering_bbox=True, row_group_size=10_000)DuckDB (with its spatial extension):
INSTALL spatial; LOAD spatial;COPY (SELECT * FROM ST_Read('in.geojson')) TO 'out.parquet' (FORMAT parquet);Whatever writes it, keep the file to the area and columns you show.
Limits
Section titled “Limits”- Geometry must be WKB, the default of GeoPandas, GDAL and DuckDB. GeoArrow-encoded geometry isn’t read.
- Coordinates must be longitude/latitude (
OGC:CRS84, the GeoParquet default, or EPSG:4326). - Compression: snappy, zstd, gzip, lz4 and brotli all work.
- Live data (
refresh:,stream:) isn’t available for GeoParquet; validation says so (Live data).
Troubleshooting
Section titled “Troubleshooting”- “…stores its geometry as GeoArrow”: rewrite it as WKB,
ogr2ogr -f Parquet -lco GEOMETRY_ENCODING=WKB out.parquet x.parquet, or GeoPandasto_parquet(..., geometry_encoding="WKB"). - “…is a Parquet file but not GeoParquet”: it has no geometry metadata,
such as a table with a WKB column written by hand. Write it with GeoPandas
to_parquet,ogr2ogr -f Parquetor DuckDB’s spatial extension. - “…is in EPSG:…”: the notice gives the
ogr2ogrcommand that reprojects it. - Your page’s script finds a property missing on a clicked feature: that
column wasn’t read. Add it to
columns:. - Hosting problems (range requests, CORS): see Large data › When something goes wrong.
Export to style.json: mlym emit writes the file’s features (every column,
or the columns: you listed) into the style as GeoJSON
(Large-data files in an exported style).