Skip to content

Commit 0c7fab7

Browse files
cholmesclaudepaleolimbot
authored
Update distribution best practices guide for GeoParquet 2.0 (#288)
* Update distribution best practices for GeoParquet 2.0 Recommend GeoParquet 2.0 (native Parquet GEOMETRY/GEOGRAPHY types with built-in geospatial statistics) over the 1.1 bbox covering column. The native per-row-group bounding box gives the same spatial-filtering speedup without a separate bbox column, producing smaller files (especially for points). - tl;dr: recommend 2.0 native types instead of "1.1 + bbox covering" - Rewrite the "bbox covering" section as "Efficient spatial access", leading with native geospatial statistics and keeping bbox covering as a documented compatibility fallback - Generalize the Spatial Ordering section away from bbox-column wording - Update the Frontend Applications section to use native statistics and fix its internal anchor link Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Distributing guide: tool guidance, page-level stats, gpio section Builds on the 2.0 shift with tool-specific and reference updates, verified against current tool releases: - GDAL/OGR: document USE_PARQUET_GEO_TYPES (NO/YES/ONLY) in 3.12+; recommend ONLY as the interim path to native geometry types (GDAL still writes GeoParquet 1.1 metadata), with a note that ONLY output is not conformant 2.0 - DuckDB: correct the section for 1.5 — GEOMETRY type and GeoParquet read/write are core (no spatial extension needed; CRS preserved on round-trip), GEOPARQUET_VERSION 'V2' writes conformant 2.0; ST_* functions (ST_Hilbert ordering, ST_Transform reprojection) still need LOAD spatial; drop the obsolete CRS-loss note - gpio: new section below DuckDB documenting the geoparquet.io CLI, which applies the best practices by default (verified against 1.3.0); remove its stale mention from Additional Tools and refresh the Spatial Partitioning reference - Overture: note it is distributed as GeoParquet 1.1 - Add a Further Discussion section explaining row-group vs page-level spatial pruning and how page-level geometry statistics could be added to Parquet (per issue #279), linked from Efficient spatial access Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Distributing guide: compression guidance and gpio compliance note - Compression: recommend zstd level 15 or higher and going as high as time allows, noting the steep diminishing returns above ~17 and that clients pay no decompression penalty (reconciles the tl;dr with the prose, which previously said "11-16") - geoparquet-io: note it produces fully compliant GeoParquet across the guide's recommendations, with 2.0 output the one area still being finalized (defaults to writing 1.1) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Split distributing guide into recommendations + tools pages Move the tool-specific how-to content out of the recommendations guide into a companion page so the main guide stays focused on the what/why. - New format-specs/distributing-geoparquet-tools.md holds "Examples in common tools" (GDAL/OGR, DuckDB, geoparquet-io, Additional Tools) plus the STAC Metadata and Spatial Partitioning how-to sections - distributing-geoparquet.md keeps the recommendations, frontend usage, exemplar datasets, and Further Discussion, with a Tool Examples pointer and cross-links from the intro and the Spatial Partitioning and STAC recommendation sections - The two pages link to each other; all anchors verified Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Tools guide: fold STAC and partitioning into per-tool sections Replace the standalone STAC Metadata and Spatial Partitioning sections with per-tool coverage: - GDAL/OGR: document spatial partitioning via `gdal vector partition` (partition on a precomputed spatial column; `gdal vector sort` to order first); note GDAL does not write STAC - DuckDB: add a spatial partitioning example using the a5 community extension with COPY ... PARTITION_BY (verified); note DuckDB does not write STAC - geoparquet-io: flesh out spatial partitioning (`gpio partition`) and STAC generation (`gpio publish stac`), both supported - Sedona: new section with the KD-tree partitioning example, noting its other distribution settings still need a PR to document - Drop Sedona from Additional Tools; update main-guide cross-links for STAC (-> geoparquet-io, plus rustac/pystac) and partitioning Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * updates from PR comments, to get across geoparquet 1.1 w/ bbox covering is good for supporting a wider array of software & versions * Update format-specs/distributing-geoparquet-tools.md Co-authored-by: Dewey Dunnington <dewey@wherobots.com> * Update format-specs/distributing-geoparquet-tools.md Co-authored-by: Dewey Dunnington <dewey@wherobots.com> * Update format-specs/distributing-geoparquet-tools.md Co-authored-by: Dewey Dunnington <dewey@wherobots.com> * Update format-specs/distributing-geoparquet-tools.md Co-authored-by: Dewey Dunnington <dewey@wherobots.com> * Update format-specs/distributing-geoparquet-tools.md Co-authored-by: Dewey Dunnington <dewey@wherobots.com> * Update format-specs/distributing-geoparquet-tools.md Co-authored-by: Dewey Dunnington <dewey@wherobots.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Dewey Dunnington <dewey@wherobots.com>
1 parent ae87363 commit 0c7fab7

2 files changed

Lines changed: 421 additions & 219 deletions

File tree

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
# Tools for Distributing GeoParquet
2+
3+
This page shows how to produce 'good' GeoParquet with common tools, applying the recommendations in
4+
[Best Practices for Distributing GeoParquet](distributing-geoparquet.md). See that guide for the reasoning behind these
5+
recommendations — compression, spatial ordering, row group size, spatial partitioning, native geometry types, and so on.
6+
7+
## Examples in common tools
8+
9+
This section discusses what each tool does by default, and shows the additional options needed to follow the
10+
[distribution recommendations](distributing-geoparquet.md), including spatial partitioning and STAC metadata where the tool
11+
supports them.
12+
13+
### GDAL/OGR
14+
15+
Out of the box:
16+
17+
```bash
18+
ogr2ogr out.parquet in.geojson
19+
```
20+
21+
Out of the box GDAL/OGR defaults to snappy compression, with max row group size of 65536.
22+
Version 3.9 and later will write out the `bbox` column by default, producing GeoParquet 1.1. There is a built-in
23+
option (`SORT_BY_BBOX=YES`) to spatially order the data that works by creating a temporary GeoPackage file and
24+
using its r-tree spatial index. It defaults to false since it can be an intensive operation,
25+
and GDAL is usually translating from formats that already have spatial indexes.
26+
27+
#### GDAL/OGR with recommended settings
28+
29+
These examples are done with the `ogr2ogr` command-line tool, but the layer creation options
30+
will be the same calling from C or Python.
31+
32+
You can control the compression and the max row group size, and the following command is sufficient
33+
if your source data is already spatially ordered in a file format with a spatial index (like FlatGeobuf or GeoPackage):
34+
35+
```bash
36+
ogr2ogr out.parquet -lco "COMPRESSION=ZSTD" -lco "MAX_ROW_GROUP_SIZE=100000" in.fgb
37+
```
38+
39+
GDAL 3.12 and above introduces `COMPRESSION_LEVEL` as a [Parquet layer creation option](https://gdal.org/en/latest/drivers/vector/parquet.html#layer-creation-options) and a new
40+
[CLI](https://gdal.org/en/latest/programs/index.html#general), which is used here.
41+
42+
```bash
43+
gdal vector convert vegetation.fgb vegetation.parquet --lco compression=zstd --lco compression_level=15
44+
```
45+
46+
If you want to be sure that the output is spatially ordered then you can add `SORT_BY_BBOX=YES`, like in the following example:
47+
48+
```bash
49+
ogr2ogr out.parquet -lco SORT_BY_BBOX=YES -lco "COMPRESSION=ZSTD" in.geojson
50+
```
51+
52+
This operation writes the data to a GeoPackage as an interim step, so it can take additional storage and computation, especially
53+
with large files, so it's not enabled by default.
54+
55+
#### Writing native geometry types
56+
57+
As of this writing GDAL does not yet write GeoParquet 2.0 metadata: by default it produces GeoParquet 1.1 with a `bbox`
58+
covering column. GDAL 3.12 and above (built against libarrow 21 or later) does, however, let you write the native Parquet
59+
`GEOMETRY`/`GEOGRAPHY` logical types via the `USE_PARQUET_GEO_TYPES` layer creation option, which takes `NO` (the default),
60+
`YES`, or `ONLY`:
61+
62+
* `YES` adds the native geometry logical types **but still** writes GeoParquet 1.1 metadata and the redundant `bbox` covering
63+
column, so the file is larger than it needs to be and still advertises itself as 1.1.
64+
* `ONLY` writes **only** the native geometry types — no `bbox` column and no `geo` metadata block. The native column carries the
65+
Parquet geospatial statistics (the per–row-group bounding box) that give efficient spatial access, and the CRS is written as
66+
PROJJSON on the logical type's `crs` property.
67+
68+
Until GDAL can emit GeoParquet 2.0 directly, `USE_PARQUET_GEO_TYPES=ONLY` is the closest you can get: it produces the native,
69+
statistics-bearing geometry column that GeoParquet 2.0 is built on, and any GeoParquet 2.0 reader can read it.
70+
71+
```bash
72+
ogr2ogr out.parquet -lco USE_PARQUET_GEO_TYPES=ONLY -lco "COMPRESSION=ZSTD" -lco "MAX_ROW_GROUP_SIZE=100000" in.fgb
73+
```
74+
75+
> [!NOTE]
76+
> A file written with `ONLY` is **not conformant GeoParquet 2.0**, because it has no `geo` metadata block (no `version`, and the
77+
> CRS is only on the native Parquet type rather than restated as PROJJSON in the `geo` metadata). It is plain Parquet with native
78+
> geospatial types — readable by GeoParquet 2.0 readers, but not a self-described GeoParquet file. Switch to a dedicated 2.0 mode
79+
> once GDAL adds one.
80+
81+
#### Spatial partitioning
82+
83+
GDAL is a flexible tool that can split a dataset into multiple files with `gdal vector partition`. It partitions on the values of
84+
one or more fields (`--field`), writing a `hive` or `flat` directory layout, and can bound the output with `--max-file-size` or
85+
`--feature-limit`:
86+
87+
```bash
88+
gdal vector partition in.parquet out_dir --field region --max-file-size 1GB
89+
```
90+
91+
GDAL does not compute a spatial partitioning scheme on its own, but you can partition spatially by first adding a column that
92+
encodes a spatial grouping — an admin region, geohash, or grid cell — and partitioning on that field. `gdal vector sort` can
93+
spatially order the features beforehand so each partition stays compact.
94+
95+
GDAL does not write STAC metadata.
96+
97+
### DuckDB
98+
99+
Out of the box:
100+
101+
```sql
102+
COPY (SELECT * FROM geo_table) TO 'out.parquet' (FORMAT 'parquet');
103+
```
104+
105+
In DuckDB 1.5 the `GEOMETRY` type and GeoParquet reading and writing are part of core DuckDB — you do **not** need the
106+
[spatial extension](https://duckdb.org/docs/stable/core_extensions/spatial/overview.html) just to read a GeoParquet file,
107+
write one, or convert/recompress/repartition it. CRS information is carried through a read/write round-trip in core as well.
108+
DuckDB automatically writes GeoParquet metadata for any output containing a geometry column. The default compression is snappy,
109+
the max row group size is 122,880, by default it writes GeoParquet 1.0.0, and the data is not spatially ordered.
110+
111+
You can choose the GeoParquet version written with the `GEOPARQUET_VERSION` copy option. Pass `GEOPARQUET_VERSION 'V2'` to write
112+
GeoParquet 2.0: the geometry column is stored using the native Parquet `GEOMETRY`/`GEOGRAPHY` logical types (with the geospatial
113+
statistics that give efficient spatial access), the CRS is written as PROJJSON, and no `bbox` covering column is added.
114+
115+
```sql
116+
COPY (SELECT * FROM geo_table) TO 'out.parquet' (FORMAT 'parquet', GEOPARQUET_VERSION 'V2');
117+
```
118+
119+
The spatial *functions* are not in core, however — anything using an `ST_*` function needs `LOAD spatial` first. That includes
120+
reprojection (`ST_Transform`) and, importantly for distribution, spatially ordering your data (`ST_Hilbert`, shown below). So in
121+
practice you will still load the extension whenever you spatially order or reproject, even though the GeoParquet writer itself
122+
does not require it.
123+
124+
#### DuckDB with recommended settings
125+
126+
You can control the [compression](https://duckdb.org/docs/sql/statements/copy.html#parquet-options), compression level and [row group size](https://duckdb.org/docs/data/parquet/tips.html#selecting-a-row_group_size), and write GeoParquet 2.0 with `GEOPARQUET_VERSION 'V2'`:
127+
128+
```sql
129+
COPY (SELECT * FROM geo_table) TO 'out.parquet' (FORMAT 'parquet', GEOPARQUET_VERSION 'V2', COMPRESSION 'zstd', COMPRESSION_LEVEL 15, ROW_GROUP_SIZE '100000');
130+
```
131+
132+
Interestingly you can also set the row group size in bytes, which would likely be a better way to handle geospatial data since the
133+
row size can vary so much.
134+
135+
```sql
136+
COPY (SELECT * FROM geo_table) TO 'out.parquet' (FORMAT 'parquet', GEOPARQUET_VERSION 'V2', COMPRESSION 'zstd', ROW_GROUP_SIZE_BYTES '128mb');
137+
```
138+
139+
The `ROW_GROUP_SIZE_BYTES` option may only be used when [`SET preserve_insertion_order = false;`](https://duckdb.org/docs/stable/guides/performance/how_to_tune_workloads#the-preserve_insertion_order-option) is enabled, which can help when working with large files, but it's not
140+
clear if it can preserve spatial ordering.
141+
142+
DuckDB also has functionality to spatially order your data, with the [`ST_Hilbert`](https://duckdb.org/docs/extensions/spatial/functions#st_hilbert)
143+
function. Because this uses `ST_*` functions you need to `LOAD spatial` first. It is strongly recommended to pass in the bounds of
144+
your entire dataset to the function call or the hilbert curve won't be built right. The following call will dynamically get the
145+
bounds of your dataset, pass that into the `ST_Hilbert` function, and write the result as GeoParquet 2.0.
146+
147+
```sql
148+
LOAD spatial;
149+
COPY (
150+
WITH bbox AS (
151+
SELECT ST_Extent(ST_Extent_Agg(geometry))::BOX_2D AS b
152+
FROM geo_table
153+
)
154+
SELECT t.*
155+
FROM geo_table AS t
156+
CROSS JOIN bbox
157+
ORDER BY ST_Hilbert(t.geometry, bbox.b)
158+
) TO 'out.parquet' (FORMAT 'parquet', GEOPARQUET_VERSION 'V2', COMPRESSION 'zstd', ROW_GROUP_SIZE '100000');
159+
```
160+
161+
DuckDB 1.5 and later preserves CRS information when you read GeoParquet in and write it back out. Earlier versions dropped the
162+
CRS metadata on write, so if you are on an older DuckDB you may need to add the CRS back in with tools like GDAL or QGIS.
163+
164+
#### Spatial partitioning
165+
166+
DuckDB can write a hive-partitioned dataset with `COPY ... PARTITION_BY`. To partition *spatially*, compute a spatial grid cell
167+
for each row and partition on it. The [a5](https://github.com/Query-farm/a5) DuckDB community extension provides a global,
168+
equal-area cell grid that works well for this:
169+
170+
```sql
171+
INSTALL a5 FROM community; LOAD a5;
172+
INSTALL spatial; LOAD spatial;
173+
COPY (
174+
SELECT *, a5_u64_to_hex(a5_lonlat_to_cell(ST_X(geometry), ST_Y(geometry), 3)) AS a5_cell
175+
FROM geo_table
176+
) TO 'partitioned' (FORMAT 'parquet', PARTITION_BY a5_cell, GEOPARQUET_VERSION 'V2', COMPRESSION 'zstd');
177+
```
178+
179+
Choose the a5 resolution to target a reasonable number of features per partition for your data. For non-point geometries, derive
180+
the cell from a representative point such as `ST_Centroid(geometry)`. You can also sort within each partition by `ST_Hilbert` for
181+
tighter row-group bounds. For other approaches, see [this gist using a KD-tree](https://gist.github.com/jwass/8e9b6c16902a05ae66b9688f1a5bb4ff)
182+
and [this blog post](https://dewey.dunnington.ca/post/2024/partitioning-strategies-for-bigger-than-memory-spatial-data/) that
183+
discusses the KD-tree along with other options (r-tree, s2 cells).
184+
185+
DuckDB does not write STAC metadata.
186+
187+
### geoparquet-io
188+
189+
[geoparquet-io](https://geoparquet.io) is a command-line tool, built on DuckDB, that is designed to apply the
190+
[distribution recommendations](distributing-geoparquet.md) by default — it exists specifically to make 'good' GeoParquet without
191+
having to remember all the options. It produces fully compliant GeoParquet that follows every recommendation in that guide; the
192+
one area still being finalized is 2.0 output, so by default it writes GeoParquet 1.1. Install it from PyPI (the package is
193+
`geoparquet-io`):
194+
195+
```bash
196+
pipx install geoparquet-io # or: pip install geoparquet-io
197+
```
198+
199+
A plain conversion applies ZSTD compression at level 15, Hilbert spatial ordering, a `bbox` covering column, and 100,000-row
200+
row groups, then validates the result:
201+
202+
```bash
203+
gpio convert geoparquet input.gpkg output.parquet
204+
```
205+
206+
It defaults to writing GeoParquet 1.1 (it auto-detects from the input, preserving the input's version and upgrading native geo
207+
types to 2.0). Pass `--geoparquet-version 2.0` to write GeoParquet 2.0, which stores the geometry in the native Parquet types
208+
with geospatial statistics and omits the `bbox` column:
209+
210+
```bash
211+
gpio convert geoparquet input.gpkg output.parquet --geoparquet-version 2.0
212+
```
213+
214+
#### Spatial partitioning
215+
216+
gpio partitions large datasets with `gpio partition`, which supports KD-tree, quadkey, S2, H3, A5, and
217+
[admin](https://medium.com/radiant-earth-insights/the-admin-partitioned-geoparquet-distribution-59f0ca1c6d96) schemes. The
218+
KD-tree scheme auto-selects a partition count targeting ~120,000 rows per file, and adds the partition column for you if it is
219+
missing:
220+
221+
```bash
222+
gpio partition kdtree input.parquet output/
223+
gpio partition kdtree input.parquet output/ --partitions 32
224+
```
225+
226+
The `gpio add` commands can also add just the partitioning column (for example `gpio add h3` or `gpio add admin-divisions`) if
227+
you want to partition or sort on it yourself.
228+
229+
#### STAC metadata
230+
231+
gpio generates STAC with `gpio publish stac`. A single file produces a STAC Item; a partitioned directory produces a STAC
232+
Collection plus per-file Items written alongside the data, following STAC best practices:
233+
234+
```bash
235+
# Single file -> STAC Item
236+
gpio publish stac input.parquet item.json --bucket s3://my-bucket/roads/
237+
238+
# Partitioned dataset -> Collection + per-file Items
239+
gpio publish stac partitions/ . --bucket s3://my-bucket/roads/
240+
```
241+
242+
You can upload the data (and its STAC) to object storage with `gpio publish upload`, and validate any file with `gpio check all`.
243+
244+
### Sedona
245+
246+
[Apache Sedona](https://sedona.apache.org/) is one of the most 'out of the box' options for spatially partitioning large
247+
datasets, using its [Spatial RDDs](https://sedona.apache.org/latest/tutorial/rdd/). The following code writes out partitions by
248+
KD-tree:
249+
250+
```python
251+
import glob
252+
253+
from sedona.spark import SedonaContext, GridType
254+
from sedona.utils.structured_adapter import StructuredAdapter
255+
from sedona.sql.st_functions import ST_GeoHash
256+
257+
# Configuring this line to do the right thing can be tricky
258+
# https://sedona.apache.org/latest/setup/install-python/?h=python#prepare-sedona-spark-jar
259+
config = (
260+
SedonaContext.builder()
261+
.config("spark.executor.memory", "6G")
262+
.config("spark.driver.memory", "6G")
263+
.getOrCreate()
264+
)
265+
266+
sedona = SedonaContext.create(config)
267+
268+
# Read from GeoParquet or some other datasource + do any spatial ops/transformations
269+
# using Sedona pyspark or SQL
270+
df = sedona.read.format("geoparquet").load(
271+
"/Users/dewey/gh/geoarrow-data/microsoft-buildings/files/microsoft-buildings_point_geo.parquet"
272+
)
273+
274+
# Create the partitioning. KDBTREE provides a nice balance providing
275+
# tight (but well-separated) partitions with approximately equal numbers of
276+
# features in each file. Note that num_partitions is only a suggestion
277+
# (actual value may differ)
278+
rdd = StructuredAdapter.toSpatialRdd(df, "geometry")
279+
rdd.analyze()
280+
281+
# UseWithoutDuplicates() variant to ensure that we don't introduce
282+
# duplicate features
283+
rdd.spatialPartitioningWithoutDuplicates(GridType.KDBTREE, num_partitions=8)
284+
rdd.getPartitioner().getGrids()
285+
df_partitioned = StructuredAdapter.toSpatialPartitionedDf(rdd, sedona)
286+
287+
# Optional: sort within partitions for tighter rowgroup bounding boxes within files
288+
df_partitioned = (
289+
df_partitioned.withColumn("geohash", ST_GeoHash(df_partitioned.geometry, 12))
290+
.sortWithinPartitions("geohash")
291+
.drop("geohash")
292+
)
293+
294+
# Write in parallel directly from each executor node.
295+
# There are several options for geoparquet writing:
296+
# https://sedona.apache.org/latest/tutorial/files/geoparquet-sedona-spark/
297+
df_partitioned.write.format("geoparquet").mode("overwrite").option("compression", "zstd").save(
298+
"buildings_partitioned"
299+
)
300+
301+
# The output files have funny names because Spark writes them this way
302+
files = glob.glob("buildings_partitioned/*.parquet")
303+
len(files)
304+
```
305+
306+
Only spatial partitioning is documented here for now. Sedona can do much more for producing distribution-ready GeoParquet
307+
(compression, row group size, GeoParquet version, etc.) — documenting those settings still needs a PR, and contributions are
308+
very welcome.
309+
310+
### Additional Tools
311+
312+
We hope to get more discussion of additional tools that follow the same format as the ones above, especially GPQ,
313+
GeoPandas, QGIS and Esri. But we'll aim to add those later as their own PR's - contributions are very welcome.

0 commit comments

Comments
 (0)