--- path: geoparquet-native-geometry title: "What native Parquet geometry types change for GeoParquet pipelines" desc: "Parquet now has GEOMETRY and GEOGRAPHY logical types. What they store, how they relate to GeoParquet 1.1 and 2.0, and what GDAL 3.12 actually writes today." date: Oct 21, 2026 author: GISOwl Engineering category: Data engineering image: /images/blog/geoparquet-native-geometry.webp imageAlt: "Dark illustration of a columnar table on the left sending orange and blue lines through small geometry icons, including points, a line, polygons and a grid, into a stack of map layers showing buildings, rivers and boundaries." imageCaption: "Cover illustration; it does not show data from this article." --- # What native Parquet geometry types change for GeoParquet pipelines 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. ## Two layers: the Parquet type and the `geo` key GeoParquet 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"](https://github.com/opengeospatial/geoparquet/blob/v1.1.0/format-specs/geoparquet.md). 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](https://github.com/apache/parquet-format/releases) (21 March 2025) added `GEOMETRY` and `GEOGRAPHY` to the schema itself. Per [LogicalTypes.md](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md) and [Geospatial.md](https://github.com/apache/parquet-format/blob/master/Geospatial.md): - Both annotate a `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, `:` such as `EPSG:4326`, `srid:`, or `projjson:` pointing at another key in the file metadata. - Sort order is undefined, and writers should not write min/max statistics for these columns. Instead, each column chunk gets `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). - For bounding boxes that cross the antimeridian, `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](https://parquet.apache.org/docs/file-format/implementationstatus/) 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. ## Where GeoParquet 2.0 stands GeoParquet 2.0 rebuilds the spec on the Parquet types. The latest published release is [v2.0.0-rc.1](https://github.com/opengeospatial/geoparquet/releases), dated 19 July 2026. [geoparquet.org/releases](https://geoparquet.org/releases/) lists it as the latest release, and the [repository README](https://github.com/opengeospatial/geoparquet) says the community has agreed on it but it is "still pending OGC approval". The main points of the [dev spec](https://github.com/opengeospatial/geoparquet/blob/main/format-specs/geoparquet.md): - Geometry columns must use the Parquet `GEOMETRY` or `GEOGRAPHY` type. Only WKB is allowed. The 1.1 GeoArrow-struct encodings are gone. - The Parquet `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`. - A new `edges` field mirrors the Parquet `algorithm` values (`planar`, `spherical`, `vincenty` and so on). - The `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](https://github.com/opengeospatial/geoparquet/commits/main/format-specs/geoparquet.md) ("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](https://github.com/opengeospatial/geoparquet/blob/main/format-specs/distributing-geoparquet.md). 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](https://guide.cloudnativegeo.org/geoparquet/index.html) 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. ## Test setup: one OSM extract, six GDAL writer modes The data is OpenStreetMap buildings for Sydney, from a [BBBike](https://download.bbbike.org/osm/bbbike/Sydney/) 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](https://opendatacommons.org/licenses/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: ```text 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](https://gdal.org/en/stable/drivers/vector/parquet.html) 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: ```bash 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.) ## What the files look like on disk `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: ```text == 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): ```text == 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`: ```text == 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](https://gdal.org/en/stable/drivers/vector/parquet.html), 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. ## What we could not test 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`](https://duckdb.org/docs/2.0/sql/statements/copy.html) has `GEOPARQUET_VERSION` with `NONE`, `V1`, `V2` and `BOTH`, and the default is `V1`. Its [geometry type page](https://duckdb.org/docs/2.0/sql/data_types/geometry.html) says only the `&&` operator in a `WHERE` clause currently uses bounding-box statistics. The [GeoPandas 1.2.0 changelog](https://github.com/geopandas/geopandas/blob/main/CHANGELOG.md) (28 September 2026) says GeoParquet 2.0 is supported for reading and writing. The GeoParquet [tools guide](https://github.com/opengeospatial/geoparquet/blob/main/format-specs/distributing-geoparquet-tools.md) says DuckDB's `V2` writes native types with PROJJSON and no covering column. ## Migration hazards Some of these we saw in the files, and some come straight from the GDAL docs. Each item says which. - **Old readers and `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". - **Column names matter more than before (observed plus inference).** GDAL kept the source name `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`. - **The CRS moves (observed).** In 1.1 the CRS is in the `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](https://github.com/opengeospatial/geoparquet/blob/main/format-specs/distributing-geoparquet-tools.md) 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. - **Statistics are per row group only (spec).** Pruning is at row-group granularity. Over HTTP, a reader that matches one 10,000-row group fetches all of it. Sort spatially and size row groups for your access pattern, as the [distribution guide](https://github.com/opengeospatial/geoparquet/blob/main/format-specs/distributing-geoparquet.md) recommends (50,000 to 150,000 rows for analytics, smaller for map clients). - **`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. ## When to switch writers to native types Based on the files above and the documented reader support: - **Write native types, `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. - **Keep writing GeoParquet 1.1 with a covering column** when you publish to readers you don't control: QGIS installs on older GDAL, pipelines pinned to pyarrow 20 or earlier, ClickHouse, arrow-go based services, or anything that only reads the `geo` key. For them a native-only file can lose the CRS or fail to open. - **Avoid GDAL's `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. - **Keep or re-add a covering column** if your main access pattern is small bbox reads over HTTP from large row groups. Page-index pruning needs it, which is why GeoParquet `main` put it back. - **Use `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. ## Sources - [Apache Parquet blog: Native geospatial types in Apache Parquet (13 Feb 2026)](https://parquet.apache.org/blog/2026/02/13/native-geospatial-types-in-apache-parquet/) - [parquet-format Geospatial.md](https://github.com/apache/parquet-format/blob/master/Geospatial.md) and [LogicalTypes.md](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md) - [parquet-format releases (2.11.0, 21 Mar 2025)](https://github.com/apache/parquet-format/releases) - [Parquet implementation status](https://parquet.apache.org/docs/file-format/implementationstatus/) - [GeoParquet repository](https://github.com/opengeospatial/geoparquet), [dev spec](https://github.com/opengeospatial/geoparquet/blob/main/format-specs/geoparquet.md), [v1.1.0 spec](https://github.com/opengeospatial/geoparquet/blob/v1.1.0/format-specs/geoparquet.md), [releases](https://github.com/opengeospatial/geoparquet/releases) - [GeoParquet distribution best practices](https://github.com/opengeospatial/geoparquet/blob/main/format-specs/distributing-geoparquet.md) and [tools guide](https://github.com/opengeospatial/geoparquet/blob/main/format-specs/distributing-geoparquet-tools.md) - [geoparquet.org](https://geoparquet.org/) and [geoparquet.org/releases](https://geoparquet.org/releases/) - [Cloud-Native Geospatial guide: GeoParquet](https://guide.cloudnativegeo.org/geoparquet/index.html) - [GDAL (Geo)Parquet driver](https://gdal.org/en/stable/drivers/vector/parquet.html) - [DuckDB COPY](https://duckdb.org/docs/2.0/sql/statements/copy.html) and [GEOMETRY type](https://duckdb.org/docs/2.0/sql/data_types/geometry.html) - [GeoPandas changelog](https://github.com/geopandas/geopandas/blob/main/CHANGELOG.md) - [BBBike Sydney extract](https://download.bbbike.org/osm/bbbike/Sydney/), data © OpenStreetMap contributors, [ODbL](https://opendatacommons.org/licenses/odbl/)