
Apache Parquet now has GEOMETRY and GEOGRAPHY logical types, so a geometry column no longer has to be an anonymous binary column that only the GeoParquet geo metadata can explain. If you write spatial Parquet today, you have to decide when to switch your writers to the native types and when to keep producing GeoParquet 1.1, and that depends on which readers sit downstream.
This post separates the two layers, says where each spec stands as of 21 October 2026, and shows what GDAL 3.12.4 writes in each mode on a real OpenStreetMap extract. We did not finish the cross-reader half of the test. The section on what we could not test explains why.
geo keyGeoParquet 1.x is metadata on top of Parquet. Geometries live in a BYTE_ARRAY column as WKB, or in GeoArrow-style structs in 1.1's "native encodings". A JSON document under the file-level geo key says which column is geometry, what its CRS is (PROJJSON), which geometry types it holds, and, since 1.1, which bbox struct column carries per-row bounding boxes. A reader that ignores the geo key sees bytes.
The Parquet format's own types work at a different level. Parquet format 2.11.0 (21 March 2025) added GEOMETRY and GEOGRAPHY to the schema itself. Per LogicalTypes.md and Geospatial.md:
BYTE_ARRAY holding ISO WKB (XY, XYZ, XYM, XYZM). The bytes on disk are the same kind of WKB GeoParquet 1.x used.GEOMETRY has planar edges and one optional parameter, crs. If crs is unset, it means OGC:CRS84.GEOGRAPHY has a crs parameter, which must be geographic, and an algorithm parameter for edge interpolation: SPHERICAL (default), VINCENTY, THOMAS, ANDOYER or KARNEY.crs is a string in one of four suggested forms: inline PROJJSON, <authority>:<code> such as EPSG:4326, srid:<identifier>, or projjson:<key_name> pointing at another key in the file metadata.GeospatialStatistics, made of a bounding box (xmin, xmax, ymin and ymax required, Z and M optional) and a list of the geometry types present as WKB integer codes (for example [3, 6] for Polygon plus MultiPolygon, or 1003 for Polygon Z).xmin may be greater than xmax. Axis order is always x/longitude first, regardless of what the CRS says.The practical change is that the schema now says "this is geometry, in this CRS". The footer also carries a per-row-group bounding box that any Parquet reader can use for pruning without knowing GeoParquet.
The Parquet implementation status page lists support for both types in Arrow C++ 21.0.0, parquet-java 1.16.0, arrow-rs 57.0.0, hyparquet 1.19.0 and DuckDB 1.4.0. Polars is read-only, and arrow-go and cudf are listed as unsupported. ClickHouse is marked unsupported, with a note that it reads the GeoParquet geo key instead. So "Parquet supports geometry" does not mean every Parquet engine handles these columns. It means the format defines them and several core libraries implement them.
GeoParquet 2.0 rebuilds the spec on the Parquet types. The latest published release is v2.0.0-rc.1, dated 19 July 2026. geoparquet.org/releases lists it as the latest release, and the repository README says the community has agreed on it but it is "still pending OGC approval". The main points of the dev spec:
GEOMETRY or GEOGRAPHY type. Only WKB is allowed. The 1.1 GeoArrow-struct encodings are gone.crs property is the source of truth and may use any Parquet form. If the file also has a geo key, its crs must be inline PROJJSON describing the same CRS, or null for unknown. On the Parquet side, unknown maps to srid:0.edges field mirrors the Parquet algorithm values (planar, spherical, vincenty and so on).geo metadata is optional for writers. A file with only native types and an EPSG:xxxx CRS is "not conformant GeoParquet 2.0", but 2.0 readers are expected to read it.The bbox covering column has moved back and forth. The rc.1 release notes say it was removed, because native row-group statistics do the same job. Then on 7 September 2026 commit 4c9f87e ("Bring back the optional bbox covering from GeoParquet 1.1", #302) put it back on main as an option. The reason is in the distribution guide. Native geospatial statistics exist only per column chunk, which means per row group. A covering column is an ordinary struct column, so it gets a page index and lets readers prune pages inside a row group. It also gives readers that predate native statistics something to filter on. If you write against "2.0", check whether your tool targets rc.1 or the current main.
One more caution: the Cloud-Native Geospatial guide's GeoParquet page still describes the 1.0 release from 2023 and does not mention 1.1, 2.0 or the native types. Use the spec repository, not that page, for current behaviour.
The data is OpenStreetMap buildings for Sydney, from a BBBike PBF extract. Its header gives https://download.BBBike.org as the source and a replication timestamp of 2026-08-21 23:00 UTC. Licence: ODbL, © OpenStreetMap contributors. We kept the multipolygons layer rows with a building tag: 275,065 features, extent 150.83°E to 151.31°E and 34.06°S to 33.68°S.
The writer is GDAL 3.12.4 "Chicoutimi" from Homebrew, built against libarrow 24.0.0. ogrinfo --format Parquet lists the relevant layer creation options:
GEOMETRY_ENCODING WKB | WKT | GEOARROW | GEOARROW_INTERLEAVED (default WKB)
WRITE_COVERING_BBOX AUTO | YES | NO (default AUTO)
USE_PARQUET_GEO_TYPES YES | NO | ONLY (default NO)
EDGES PLANAR | SPHERICAL (default PLANAR)
ARROW_VERSION=24.0.0
This GDAL has no option for choosing a GeoParquet version. GDAL 3.13.3, released 18 August 2026, is newer. A diff of its Parquet driver docs against 3.12.4 shows only COVERING_BBOX_NAME and TIMESTAMP_WITH_OFFSET as new options, so nothing changes for native types.
We made two spatially sorted sources, one reprojected to GDA2020 / MGA zone 56 (EPSG:7856), the projected CRS a Sydney team would use, and one left in WGS 84. Then we wrote six files from them:
ogr2ogr -f GPKG sydney_buildings_4326.gpkg sydney.osm.pbf multipolygons \
-where "building IS NOT NULL" -select osm_id,osm_way_id,name,building \
-nln buildings -nlt PROMOTE_TO_MULTI
ogr2ogr src/sydney_7856_sorted.parquet sydney_buildings_4326.gpkg buildings \
-t_srs EPSG:7856 -lco SORT_BY_BBOX=YES -lco ROW_GROUP_SIZE=10000 -lco COMPRESSION=ZSTD
ogr2ogr src/sydney_4326_sorted.parquet sydney_buildings_4326.gpkg buildings \
-lco SORT_BY_BBOX=YES -lco ROW_GROUP_SIZE=10000 -lco COMPRESSION=ZSTD
C="-lco ROW_GROUP_SIZE=10000 -lco COMPRESSION=ZSTD -nln buildings"
ogr2ogr out/gdal_v11.parquet src/sydney_7856_sorted.parquet $C
ogr2ogr out/gdal_native_yes.parquet src/sydney_7856_sorted.parquet $C -lco USE_PARQUET_GEO_TYPES=YES
ogr2ogr out/gdal_native_only.parquet src/sydney_7856_sorted.parquet $C -lco USE_PARQUET_GEO_TYPES=ONLY
ogr2ogr out/gdal_native_only_4326.parquet src/sydney_4326_sorted.parquet $C -lco USE_PARQUET_GEO_TYPES=ONLY
ogr2ogr out/gdal_geography_only.parquet src/sydney_4326_sorted.parquet $C -lco USE_PARQUET_GEO_TYPES=ONLY -lco EDGES=SPHERICAL
ogr2ogr out/gdal_geography_yes.parquet src/sydney_4326_sorted.parquet $C -lco USE_PARQUET_GEO_TYPES=YES -lco EDGES=SPHERICAL
(In zsh, $C does not word-split. We ran these from a bash script with an array.)
parquet-dump-schema from Homebrew's apache-arrow package shows the difference directly. In the default 1.1 file, the geometry column has no logical type at all:
== gdal_v11.parquet
optional binary field_id=-1 geom;
optional group field_id=-1 geom_bbox {
required float field_id=-1 xmin;
required float field_id=-1 ymin;
required float field_id=-1 xmax;
required float field_id=-1 ymax;
With USE_PARQUET_GEO_TYPES=ONLY on the projected data, the column is typed, and GDAL puts the full PROJJSON for EPSG:7856 into the logical type's crs (shortened here):
== gdal_native_only.parquet
optional binary field_id=-1 geom (Geometry(crs={"$schema":"https://proj.org/schemas/v0.7/projjson.schema.json",
"type":"ProjectedCRS","name":"GDA2020 / MGA zone 56", ... "id":{"authority":"EPSG","code":7856}}));
On WGS 84 data, GDAL leaves crs empty, which by the Parquet spec means OGC:CRS84. With EDGES=SPHERICAL it switches to GEOGRAPHY:
== gdal_native_only_4326.parquet
optional binary field_id=-1 geom (Geometry(crs=));
== gdal_geography_only.parquet
optional binary field_id=-1 geom (Geography(crs=, algorithm=spherical));
== gdal_geography_yes.parquet
optional binary field_id=-1 geom (Geography(crs=, algorithm=spherical));
optional group field_id=-1 geom_bbox { required float xmin; ... }
YES mode keeps the geom_bbox covering column and, per the GDAL docs, still writes file-level GeoParquet metadata. The schema dumps confirm the covering column. We did not dump the geo JSON. The covering values are single-precision float. That is fine for a conservative filter box, but don't treat them as exact coordinates.
All files have 275,065 rows, at most 10,000 rows per row group, and ZSTD compression:
| File | Writer mode | Geometry column | bbox column | Size (bytes) |
|---|---|---|---|---|
| gdal_v11.parquet | default (NO), EPSG:7856 |
plain binary + geo key |
yes | 37,100,510 |
| gdal_native_yes.parquet | YES, EPSG:7856 |
GEOMETRY(crs=PROJJSON) |
yes | 37,028,662 |
| gdal_native_only.parquet | ONLY, EPSG:7856 |
GEOMETRY(crs=PROJJSON) |
no | 32,564,042 |
| gdal_native_only_4326.parquet | ONLY, WGS 84 |
GEOMETRY(crs=) |
no | 28,736,339 |
| gdal_geography_yes.parquet | YES + SPHERICAL, WGS 84 |
GEOGRAPHY(algorithm=spherical) |
yes | 32,150,267 |
| gdal_geography_only.parquet | ONLY + SPHERICAL, WGS 84 |
GEOGRAPHY(algorithm=spherical) |
no | 28,735,159 |
Dropping the covering column saved 4,464,620 bytes (12.0%) on the projected file and 3,415,108 bytes (10.6%) on the geographic one. YES mode costs almost the same as plain 1.1, because it keeps the covering column. These are polygon footprints. For point data the covering column is a bigger share of each row, so the saving should be larger, but we did not measure that.
This run did not test the readers: DuckDB 2.0.0, PyArrow, GeoPandas 1.2.0 and Polars were never installed (they failed to download in time), and we did not read the files back with ogrinfo, check row-group pruning, or time any bbox query. Closing that gap takes reading each of the six files with each of those readers, recording the geometry and CRS they report, and logging the bytes fetched for a bbox query over HTTP range requests.
Here is what their documentation says, as claims to verify rather than results. DuckDB's COPY has GEOPARQUET_VERSION with NONE, V1, V2 and BOTH, and the default is V1. Its geometry type page says only the && operator in a WHERE clause currently uses bounding-box statistics. The GeoPandas 1.2.0 changelog (28 September 2026) says GeoParquet 2.0 is supported for reading and writing. The GeoParquet tools guide says DuckDB's V2 writes native types with PROJJSON and no covering column.
Some of these we saw in the files, and some come straight from the GDAL docs. Each item says which.
ONLY files (documented). GDAL's driver docs say an ONLY file is fully compatible only with GDAL 3.12 or later and libarrow 21 or later. With libarrow 20, the geometry column is recognised only if its name is in GEOM_POSSIBLE_NAMES (default geometry,wkb_geometry,wkt_geometry), and the CRS is not recognised. With older libarrow, the file "cannot be opened at all".geom, which is not in that default list. Going by the documented behaviour, a libarrow-20 GDAL would not find the geometry in our ONLY files. We did not run that version. If you write ONLY, call the column geometry.geo JSON. In ONLY mode it exists only inside the Parquet logical type, so a tool that reads the geo key and ignores logical types gets no CRS from these files. Default WGS 84 is written as an empty crs, not as an explicit string. A reader that treats an empty string as "unknown" instead of "OGC:CRS84" would be wrong. The spec is clear on this, and implementations are a separate question.YES mode labels itself 1.1 (documented). It adds native types but keeps 1.1 metadata and the covering column, which is why its size above is close to plain 1.1. The GeoParquet tools guide also says ONLY files lack the geo block and are therefore not conformant 2.0. GDAL 3.12 and 3.13 cannot produce conformant 2.0.GEOGRAPHY changes semantics, not just the label. EDGES=SPHERICAL tells readers that polygon edges are great-circle arcs. For building footprints a few metres across the difference is negligible. For large polygons it changes containment results, and a planar-only engine reading GEOGRAPHY may refuse the column or treat it as planar. We did not test which.Based on the files above and the documented reader support:
ONLY or a tool's 2.0 mode, when every consumer is on Arrow C++ 21 or later, GDAL 3.12 or later, DuckDB 1.4 or later, arrow-rs 57 or later, or parquet-java 1.16 or later. Also write native when the files go into a non-spatial engine or a table format that should see a typed geometry column instead of opaque binary. You get the CRS in the schema, per-row-group bounding boxes any implementing Parquet reader can use, and on this dataset a file about 11 to 12% smaller.geo key. For them a native-only file can lose the CRS or fail to open.YES mode as a long-term target. It is a transition format, with both mechanisms in one file and a 1.1 label. It is useful for a release or two while you find out which consumers break.main put it back.GEOGRAPHY only if the data really has geodesic edges, for example exports from BigQuery or Snowflake geography columns. For projected engineering data like the MGA 56 file here, GEOMETRY with an explicit CRS is the correct type.The next test should take these six files through DuckDB 2.0, GeoPandas 1.2 and PyArrow, and record which ones honour the empty-crs default and whether DuckDB's && filter skips row groups on native statistics alone, with no covering column present.