Adding Spatial Indices & Metadata¶
The add commands enhance GeoParquet files with spatial indices, geometry metrics, and administrative metadata.
The output keeps the input's GeoParquet version and CRS
Every add command rewrites the whole file to append its column, and with
no --geoparquet-version that rewrite preserves what the input was: a 1.1
file stays 1.1, a 2.0 file stays 2.0, and a file that carries a native
Parquet GEOMETRY type with no geo key is written as native 2.0. Until
#993 that last
case was re-encoded as 1.1 WKB, which stripped the logical type and the
CRS stored inside it — an EPSG:5070 file came back out declaring
nothing, which every reader resolves as OGC:CRS84. Pass
--geoparquet-version to ask for something other than the input's version;
the CRS is preserved either way, including when you ask for 1.1, which has
nowhere but the geo block to record it.
A native 2.0 output needs a reader that understands the Parquet GEOMETRY
logical type — older pyarrow, geopandas and DuckDB builds may not open one.
--geoparquet-version 1.1 is the escape hatch: it writes plain WKB, and
keeps the CRS in the geo block.
It is the same rewrite behind gpio sort quadkey and
gpio partition quadkey/h3/s2/a5/kdtree, which add their index column to a
scratch file first, so those commands preserve the version and the CRS now
too.
Two shapes are not fully covered yet, both pinned by failing-on-purpose
tests: a native GEOGRAPHY column comes back as a planar GEOMETRY while
the metadata still declares spherical edges
(#999), and a
secondary geometry column keeps its own Parquet type but is left out of
geo.columns and loses its CRS
(#1000).
Coordinate Reference Systems
The grid-index commands (h3, s2, a5, quadkey) and admin-divisions are
CRS-aware. If your input declares a projected CRS (e.g. EPSG:5070 or
EPSG:28992), the geometry centroid is reprojected to OGC:CRS84 (lon/lat
degrees) before the cell is computed, and the input is reprojected to the
admin boundaries' CRS before the spatial join — so you no longer need to run
gpio convert reproject first. Inputs without a declared CRS are treated as
OGC:CRS84 per the GeoParquet spec.
Bounding Boxes¶
Add precomputed bounding boxes for faster spatial queries:
gpio add bbox input.parquet output.parquet
# Works with remote files
gpio --aws-profile prod add bbox s3://bucket/input.parquet s3://bucket/output.parquet
import geoparquet_io as gpio
gpio.read('input.parquet').add_bbox().write('output.parquet')
# Custom column name
gpio.read('input.parquet').add_bbox(column_name='bounds').write('output.parquet')
Creates a struct column with {xmin, ymin, xmax, ymax} for each feature, plus the covering metadata that points at it. Because gpio computes the column from the geometry in the same statement, it can declare the covering. For a column gpio did not compute, the only name it will vouch for is a struct column called exactly bbox — the universal convention every 1.0-era writer emitted before covering existed, which any write carries forward with a covering. Any other name (bounds, extent, tile_bbox) is left undeclared, since a covering asserts a relationship gpio cannot verify from a column name; gpio add bbox-metadata is where you assert it deliberately. Inventing one is the only thing the name rule governs: a covering that already exists — one gpio just computed under --bbox-name bounds, or one GDAL wrote over geometry_bbox — is somebody's assertion already, and every write carries it forward whatever the column is called. The covering key is not part of the GeoParquet 2.0 specification text — it was introduced in 1.1 and removed in 2.0 in favour of the native statistics. 2.0 readers must tolerate unknown fields, so a covering stays legal to carry, and geoparquet#302 proposes reinstating it as an option (still open at time of writing). The motivation is real either way: native statistics prune whole row groups, while a bbox column's page index also prunes pages within one.
Existing Bbox Detection¶
An "existing bbox column" means the same two things everywhere in gpio: the
column the primary geometry column's own covering.bbox names, or a struct
column called exactly bbox that no other column's covering claims. A
bounds, tile_extent or boundary_bbox struct nothing declared is ordinary
data — gpio add bbox computes its own column beside it,
gpio extract --bbox filters on the geometry rather than pre-filtering on
values nothing vouched for, and gpio add bbox-metadata reports that there is
no bbox column to declare unless you name one with --bbox-name.
The command automatically checks for existing bbox columns:
- If bbox exists with metadata: Nothing is recomputed, and the input is copied to the output file unchanged
- If bbox exists without metadata: Same copy, plus a suggestion to use
gpio add bbox-metadata - If
--bbox-nameasks for a different name: That column is computed and added alongside the existing one, with a warning that the file will have two bbox columns - If a write option is asked for explicitly (
--geoparquet-version,--compression,--compression-level,--row-group-size): the column is recomputed instead of copied, because a verbatim copy reproduces the input's settings and cannot honour the requested ones - Use
--force: Replace existing bbox column with a freshly computed one - If the name is taken by something that is not a bbox struct (a string tile id called
bbox, or the same name spelledBBOX): that column is the input's own data, so it is kept as it is and the computed bbox is written under the next free name (bbox_1,bbox_2, …) with the covering declared over that column; a warning names both.gpio convertand the Python API'sTable.add_bbox()take the same decision, so one collision means one thing whichever path writes the column (#1176)
The copy matters for pipelines: gpio add bbox in.parquet out.parquet always leaves a file at out.parquet with a bbox column, whether or not one had to be computed, so the next step in a script has something to read (#728). Every copy is reported on the console, so a recomputed bbox is never confused with a copied one. --dry-run says which of the two it would be. When no output file is given there is nothing to write, and the command only reports.
The same holds for a stream: ... | gpio add bbox - out.parquet on an input that already has a bbox column passes the data through untouched and declares no covering for a column gpio did not compute.
That copy is a remote write like any other, so it goes through the object store gpio is configured to use: --s3-endpoint, --s3-region, --s3-no-ssl and --aws-profile apply to it exactly as they do to a recomputed output, and a copy to MinIO no longer ends up pointed at AWS (#810). Inputs may be local paths or s3://, gs:// and https:// URLs — an https:// input is fetched with a plain streaming GET of the URL exactly as given, so a presigned URL keeps its signature. Outputs may be local paths or s3://, gs:// and az:// URLs. az:// is a write destination only: reading from Azure is not supported, and an az:// input is refused up front with a message saying so, rather than failing deep in the metadata probe. An Azure output URL is written account-first, az://<account>/<container>/<path>, with the credential taken from AZURE_STORAGE_ACCOUNT_KEY or AZURE_STORAGE_SAS_TOKEN (#864); the abfs://, abfss:// and azure:// spellings order those parts differently and are refused rather than mis-read. Any scheme the copy cannot serve is refused by name before anything is read, rather than failing partway through.
The copy resolves S3 credentials the same way gpio publish upload does: through the full AWS chain — environment keys, --aws-profile, ~/.aws/credentials, and the SSO, assume-role and credential_process configurations botocore supports (#865) — so a session that can read the input can also write the copy. Credentials are resolved once at the start of the write; a session token that expires mid-upload is not refreshed.
# Already has a bbox: copies input to output, computing nothing
gpio add bbox input.parquet output.parquet
# Force replace existing bbox
gpio add bbox input.parquet output.parquet --force
Options:
# Custom column name
gpio add bbox input.parquet output.parquet --bbox-name bounds
# Force replace existing bbox
gpio add bbox input.parquet output.parquet --force
# With compression settings
gpio add bbox input.parquet output.parquet --compression ZSTD --compression-level 15
# Dry run (preview SQL)
gpio add bbox input.parquet output.parquet --dry-run
Add Bbox Metadata Only¶
If your file already has a bbox column but lacks covering metadata (e.g., from external tools):
gpio add bbox-metadata myfile.parquet
This modifies the file in-place to add only the metadata, without creating a new file.
The data is not re-encoded
The geo key is rewritten in the footer and every page below it is copied
byte for byte (#1141),
so the compression codec and its level, the encodings, the bloom filters,
the page index, the row-group boundaries, the row order and every other
footer key come out exactly as they went in. The file grows by the size of
the key and nothing else, and the work is proportional to the footer rather
than to the file.
This matters because a Parquet file does not record the compression level
it was written at. The command used to re-encode every page with a DuckDB
COPY, which had no level to carry over and fell back to DuckDB's default
of 3: a file written at zstd 15 came back 11.6% bigger on the reported
fixture and 36% bigger on a real one. A footer gpio cannot read — an
encrypted one, or a thrift structure it does not recognise — falls back to
that rewrite, with a warning saying so.
Requires GeoParquet 1.1+
The covering key was introduced in GeoParquet 1.1, so this command fails on a
file declaring 1.0 rather than writing metadata that version cannot carry.
Convert it first: gpio convert geoparquet in.parquet out.parquet --geoparquet-version 1.1.
Requires the spec's struct field order
GeoParquet 1.1 fixes the order of a bbox column's struct fields —
xmin, ymin, xmax, ymax, or xmin, ymin, zmin, xmax, ymax, zmax. Overture
writes xmin, xmax, ymin, ymax, which holds exactly the right four numbers
and still cannot be pointed at by a covering; declaring one anyway produces
a file gpio check spec rejects. The same goes for a struct whose fields
are not FLOAT or DOUBLE. This command fails on such a column rather than
writing it, and every other gpio write leaves the column undeclared.
Rewrite the column in the spec's order first:
gpio add bbox --force in.parquet out.parquet. Note that --force on a
1.0 input writes the output at 1.1, since that is what carries the key.
Requires existing GeoParquet metadata
The command adds a covering key to metadata that already describes the
geometry column. On plain Parquet with no geo key it fails rather than
inventing a geo block with no encoding and no geometry_types. Make it
GeoParquet first: gpio convert geoparquet in.parquet out.parquet.
Table.add_bbox_metadata() and ops.add_bbox_metadata() refuse the same
input with the same error. A file with no bbox column also fails, rather
than printing the error and exiting 0.
import geoparquet_io as gpio
# Add bbox column, then add covering metadata
table = gpio.read('input.parquet')
table_with_bbox = table.add_bbox().add_bbox_metadata()
table_with_bbox.write('output.parquet')
# Or just add metadata if bbox column already exists
table = gpio.read('file_with_bbox.parquet')
table.add_bbox_metadata().write('output.parquet')
Declaring a column gpio will not vouch for¶
Auto-detection names two columns and no others: the one the primary geometry
column's own covering.bbox already points at, and a struct called exactly
bbox that no other column's covering claims. A file GDAL wrote carries its
struct as geometry_bbox — exactly the right four numbers, under a name that is
not evidence of what they bound. --bbox-name is where you supply that
evidence:
gpio add bbox-metadata gdal_written.parquet --bbox-name geometry_bbox
import geoparquet_io as gpio
table = gpio.read('gdal_written.parquet')
table.add_bbox_metadata(bbox_column='geometry_bbox').write('output.parquet')
Naming a column is an assertion about its values, so three things stay refused.
A column the file does not have. A struct the spec forbids a covering to point
at (Overture's xmin, xmax, ymin, ymax) — see the field-order note above. And a
struct some other geometry column already declares as its covering: those
values bound that column's geometry, not the primary's, so declaring them on the
primary would make readers prune rows that genuinely match. Compute the
primary's own column instead with gpio add bbox.
STAC Collection Metadata¶
A stac-geoparquet file holds
STAC Items as rows. The specification lets it carry the Collections those
Items belong to in the file's own footer, under the stac-geoparquet key, so a
reader that has the file has the Collection too and does not need a catalog to
interpret it. gpio add stac-metadata writes that key:
The setup each example below runs first — copying a file to items.parquet
and writing a small collection.json — lives in a doctest: comment above
each fence, so the examples execute in the docs lane rather than being skipped.
# In place
gpio add stac-metadata items.parquet --collection collection.json
# Or to a copy
gpio add stac-metadata items.parquet out.parquet --collection collection.json
from geoparquet_io.api import ops
# The file form patches the footer of a file already on disk
ops.add_stac_metadata('items.parquet', 'collection.json')
import geoparquet_io as gpio
# Or set it before the file is written
table = gpio.read('items.parquet')
table.add_stac_metadata('collection.json').write('out.parquet')
The value written is:
{"version": "1.1.0", "collections": {"PSScene": {"type": "Collection", "id": "PSScene"}}}
collections is a map, and the singular collection is deprecated
collections is keyed by collection id, which is what an Item's
collection field points at. A Collection with no id is refused rather
than filed under a made-up key. stac-geoparquet 1.0.0 defined a singular
collection key instead; 1.1.0 deprecated it, so under the default
1.1.0 gpio never writes it. With --stac-geoparquet-version 1.0.0 gpio
writes collection (for a strict 1.0.0 reader) beside the map, and refuses
a file that would name more than one Collection, which 1.0.0 cannot
express. Those are the only two versions the specification defines;
anything else is refused, because the version in the footer is what a
reader dispatches on.
The rows should be STAC Items, and the Collection should be one
gpio warns, but does not refuse, when the file lacks a column the
specification requires of an Item row (id, geometry, bbox, links,
assets, stac_extensions), or when the object passed is not of type
Collection. The Collection is embedded as given — including any signed
hrefs in its links — so check it before publishing the file.
The data is not re-encoded
Like gpio add bbox-metadata, this rewrites the footer and copies every
page below it byte for byte, so the compression codec and its level, the
encodings, the bloom filters, the page index, the row-group boundaries and
the row order all come out as they went in. Unlike add bbox-metadata
there is no rewrite behind it: a footer gpio cannot patch is reported
rather than re-encoded, since re-encoding a whole file to add one key
would cost its compression level to buy nothing. The file is still copied
once, so the command needs room for a second copy beside it.
Remote URLs are not supported — download the file, add the key, upload it.
A second call adds a collection, it does not replace the map
collections is a map precisely so one stac-geoparquet file can index
Items from more than one Collection. Running gpio add stac-metadata
again with a different --collection adds that Collection alongside the
ones already in the footer; running it again with the same collection id
replaces that one entry. A 1.0.0 file's singular collection, and any
other top-level field, is carried over too. The version declared by the
most recent call wins.
H3 Hexagonal Cells¶
Input CRS
The grid commands (h3, s2, a5, quadkey) key on lon/lat. Input in a
non-CRS84 CRS is reprojected to lon/lat automatically before cell assignment,
so projected data (e.g. national grids in metres) keys correctly with no
manual reprojection. Input already in OGC:CRS84 / EPSG:4326 is untouched.
Index entries in the covering metadata
The index commands (h3, s2, a5, quadkey, kdtree) record their
column in the geometry's covering metadata only beside a bbox entry —
the one covering encoding the GeoParquet spec defines, and the one readers
such as geopandas require. An input with no bbox column gets no covering at
all, and adding the bbox column later does not bring the index entry back;
run gpio add bbox first if you want it recorded.
Add H3 hexagonal cell IDs based on geometry centroids:
gpio add h3 input.parquet output.parquet --resolution 9
# From HTTPS to S3
gpio add h3 https://example.com/data.parquet s3://bucket/indexed.parquet --resolution 9
import geoparquet_io as gpio
gpio.read('input.parquet').add_h3(resolution=9).write('output.parquet')
# Custom column name
gpio.read('input.parquet').add_h3(column_name='h3_index', resolution=13).write('output.parquet')
Resolution guide:
- Resolution 7: ~5 km² cells
- Resolution 9: ~105 m² cells (default)
- Resolution 11: ~2 m² cells
- Resolution 13: ~0.04 m² cells
Options:
# Custom column name
gpio add h3 input.parquet output.parquet --h3-name h3_index
# Different resolution
gpio add h3 input.parquet output.parquet --resolution 13
# With row group sizing
gpio add h3 input.parquet output.parquet --row-group-size-mb 256MB
S2 Spherical Cells¶
Add S2 spherical cell IDs based on geometry centroids.
S2 cells are computed by the geography DuckDB community extension,
which gpio installs on first use:
gpio add s2 input.parquet output.parquet --level 13
# From HTTPS to S3
gpio add s2 https://example.com/data.parquet s3://bucket/indexed.parquet --level 13
import geoparquet_io as gpio
gpio.read('input.parquet').add_s2(level=13).write('output.parquet')
# Custom column name
gpio.read('input.parquet').add_s2(column_name='s2_index', level=18).write('output.parquet')
S2 uses Google's Spherical Geometry library which divides the Earth's surface into a hierarchy of cells using quadtree subdivision. Unlike H3's hexagonal grid, S2 cells are variable quads that provide hierarchical spatial indexing.
Level guide:
| Level | Approx. Area | Use Case |
|---|---|---|
| 0 | 85M km² | Global |
| 1 | 21M km² | Continent |
| 2 | 5.3M km² | Large country |
| 3 | 1.3M km² | Medium country |
| 4 | 324,000 km² | Small country |
| 5 | 81,000 km² | State/province |
| 6 | 20,000 km² | Large city |
| 7 | 5,100 km² | Metropolitan area |
| 8 | 1,250 km² | City |
| 9 | 313 km² | District |
| 10 | 78 km² | Neighborhood cluster |
| 11 | 20 km² | Large neighborhood |
| 12 | 4.9 km² | Neighborhood |
| 13 | 1.2 km² | Small neighborhood (default) |
| 14 | 0.31 km² | Street cluster |
| 15 | 77,000 m² | City block |
| 16 | 19,000 m² | Large building complex |
| 17 | 4,800 m² | Building complex |
| 18 | 1,200 m² | Large building |
| 19 | 300 m² | Building |
| 20 | 75 m² | Building section |
| 21 | 19 m² | Room cluster |
| 22 | 4.7 m² | Room |
| 23 | 1.2 m² | Furniture |
| 24 | 0.29 m² | Small object |
| 25 | 0.07 m² | Tiny object |
| 26 | 0.02 m² | Precision object |
| 27 | 0.005 m² | High precision |
| 28 | 0.001 m² | Very high precision |
| 29 | 0.0003 m² | Ultra precision |
| 30 | 0.00007 m² | Maximum precision (~0.7 cm²) |
Options:
# Custom column name
gpio add s2 input.parquet output.parquet --s2-name s2_index
# Different level
gpio add s2 input.parquet output.parquet --level 18
# With row group sizing
gpio add s2 input.parquet output.parquet --row-group-size-mb 256MB
Technical Details¶
S2 cell IDs are computed using DuckDB's geography extension:
s2_cell_token(
s2_cell_parent(
s2_cellfromlonlat(
ST_X(ST_Centroid(geometry)),
ST_Y(ST_Centroid(geometry))
),
level
)
)
- s2_cellfromlonlat: Converts lon/lat to S2 cell at maximum precision (level 30)
- s2_cell_parent: Gets parent cell at desired level
- s2_cell_token: Converts to hex token string for portability
Cell IDs are stored as hex strings (e.g., "89c25901") rather than integers for
maximum portability across systems.
A5 Cells¶
Add A5 spatial cell IDs based on geometry centroids.
gpio add a5 input.parquet output.parquet --resolution 15
# From HTTPS to S3
gpio add a5 https://example.com/data.parquet s3://bucket/indexed.parquet --resolution 15
import geoparquet_io as gpio
gpio.read('input.parquet').add_a5(resolution=15).write('output.parquet')
# Custom column name
gpio.read('input.parquet').add_a5(column_name='a5_index', resolution=12).write('output.parquet')
Options:
# Custom column name
gpio add a5 input.parquet output.parquet --a5-name a5_index
# Different resolution
gpio add a5 input.parquet output.parquet --resolution 12
# With row group sizing
gpio add a5 input.parquet output.parquet --row-group-size-mb 256MB
KD-Tree Partitions¶
Add balanced spatial partition IDs using KD-tree:
# Auto-select partitions (default: ~120k rows each)
gpio add kdtree input.parquet output.parquet
# Explicit partition count (must be power of 2)
gpio add kdtree input.parquet output.parquet --partitions 32
# Exact mode (deterministic but slower)
gpio add kdtree input.parquet output.parquet --partitions 16 --exact
import geoparquet_io as gpio
# Auto mode: size the tree from the row count, like the bare CLI call
gpio.read('input.parquet').add_kdtree(auto=True).write('output.parquet')
# A different target, still auto
gpio.read('input.parquet').add_kdtree(
auto=True, target_rows=50000
).write('output.parquet')
# Custom column name and an explicit iteration count
gpio.read('input.parquet').add_kdtree(
column_name='partition_id',
iterations=5 # 2^5 = 32 partitions
).write('output.parquet')
Breaking change for the Python API: the implicit iterations=9 is gone
Table.add_kdtree() and ops.add_kdtree() used to fall back to
iterations=9 — 512 partitions whatever the input — where the bare CLI call
sizes the tree from the row count. Such a call now raises
InvalidParameterError. Pass auto=True to get what the CLI gives you, or
pass iterations=9 to keep the output you had.
Auto mode (default): - Targets ~120k rows per partition - Uses approximate computation (O(n)) - Fast on large datasets
Explicit mode: - Specify partition count (2, 4, 8, 16, 32, ...) - Control granularity
Exact vs Approximate: - Approximate: O(n), samples 100k points - Exact: O(n × log₂(partitions)), deterministic
Options:
# Custom target rows per partition
gpio add kdtree input.parquet output.parquet --auto 200000
# Custom sample size for approximate mode
gpio add kdtree input.parquet output.parquet --approx 200000
# Track progress
gpio add kdtree input.parquet output.parquet --verbose
Geometry Metrics¶
Add geodesic area and perimeter measurements to each feature:
gpio add geometry-metrics input.parquet output.parquet
# Preview SQL without executing
gpio add geometry-metrics input.parquet output.parquet --dry-run
# Without Vecorel metadata
gpio add geometry-metrics input.parquet output.parquet --no-vecorel
import geoparquet_io as gpio
from geoparquet_io.core.add.geometry_metrics import add_geometry_metrics
# Add area and perimeter columns
add_geometry_metrics('input.parquet', 'output.parquet')
# Without Vecorel schema metadata
add_geometry_metrics('input.parquet', 'output.parquet', vecorel=False)
Calculates two columns using WGS84 spheroid-based calculations:
metrics:area— geodesic area in square meters (m²)metrics:perimeter— geodesic perimeter in meters (m)
By default, the output follows the Vecorel geometry-metrics extension specification. This adds collection metadata to the Parquet file referencing the schema URL, and ensures required columns (id, geometry) are present and non-nullable. Use --no-vecorel to skip the metadata and just add the raw metric columns.
Options:
# With compression settings
gpio add geometry-metrics input.parquet output.parquet --compression ZSTD --compression-level 15
# With row group sizing
gpio add geometry-metrics input.parquet output.parquet --row-group-size-mb 256MB
# Show generated SQL
gpio add geometry-metrics input.parquet output.parquet --show-sql
Input CRS
The spheroid calculations assume WGS84 (EPSG:4326) input. If your data uses a different CRS, reproject first with gpio convert reproject.
Administrative Divisions¶
Add administrative division columns via spatial join with remote boundaries datasets:
How It Works¶
Performs spatial intersection between your data and remote admin boundaries to add admin division columns. Uses efficient spatial extent filtering to query only relevant boundaries from remote datasets.
Input in a non-CRS84 CRS is reprojected to the boundaries' CRS (OGC:CRS84) before the intersection, so projected data joins correctly instead of erroring on a CRS mismatch. (For a non-CRS84 input the bbox pre-filter is skipped, since the stored bbox is in the source CRS — the extent filter still bounds the query.)
For native-geometry inputs (GeoParquet 2.0 and GeoParquet 1.1 with geoarrow encoding), the spatial join relies on native Parquet column statistics to skip irrelevant data, but this is not quite working yet, so expect those to be a bit slower until #462 is implemented. For non-native inputs (GeoParquet 1.x that carry a bbox column), a cheap bbox-overlap test is applied before ST_Intersects to prune candidates, and using it will be 5-10x faster.
The join is a streaming LEFT JOIN so it scales to very large inputs (hundreds of millions of features) with bounded memory. A feature is attributed to the admin polygon(s) it intersects; because the per-level caches are non-overlapping, this is normally exactly one polygon per level. A feature straddling overlapping polygons at a border is emitted once per match. De-duplicating such border overlaps is intentionally left to a future dedicated operation rather than folded into this join, as including it greatly degraded performance.
Quick Start¶
# Add all GAUL levels (continent, country, department)
gpio add admin-divisions input.parquet output.parquet --dataset gaul
# Preview SQL before execution
gpio add admin-divisions input.parquet output.parquet --dataset gaul --dry-run
import geoparquet_io as gpio
# Add all GAUL levels (continent, country, department) -- the default,
# exactly like the CLI with no --dataset/--levels
table = gpio.read('input.parquet')
enriched = table.add_admin_divisions()
enriched.write('output.parquet')
# Or name the levels explicitly
enriched = table.add_admin_divisions(dataset='gaul', levels=['country'])
enriched.write('output.parquet')
Multi-Level Admin Divisions¶
Add multiple hierarchical administrative levels:
# Add all GAUL levels (adds admin:continent, admin:country, admin:department)
gpio add admin-divisions buildings.parquet output.parquet --dataset gaul
# Add specific levels only
gpio add admin-divisions buildings.parquet output.parquet --dataset gaul \
--levels continent,country
# Use Overture Maps dataset
gpio add admin-divisions buildings.parquet output.parquet --dataset overture \
--levels country,region
import geoparquet_io as gpio
# Add multiple levels
table = gpio.read('buildings.parquet')
enriched = table.add_admin_divisions(
dataset='gaul',
levels=['continent', 'country', 'department']
)
enriched.write('output.parquet')
# Overture dataset, pinning the pre-1.4 column prefix
enriched = table.add_admin_divisions(
dataset='overture',
levels=['country', 'region'],
prefix='overture'
)
Vecorel-Compliant Output¶
Use --vecorel for output that follows the Vecorel administrative division extension:
# Vecorel-compliant admin columns (uses Overture dataset automatically)
gpio add admin-divisions input.parquet output.parquet --vecorel
# Equivalent to:
gpio add admin-divisions input.parquet output.parquet \
--dataset overture --levels country,region --prefix admin
import geoparquet_io as gpio
table = gpio.read('input.parquet')
enriched = table.add_admin_divisions(
dataset='overture',
levels=['country', 'region'],
vecorel=True
)
enriched.write('output.parquet')
The --vecorel flag:
- Forces the Overture dataset (overrides
--dataset) - Sets levels to country,region (overrides
--levels) - Adds
admin:country_code(ISO 3166-1 alpha-2) andadmin:subdivision_code(ISO 3166-2) - Writes
collectionmetadata with the Vecorel schema URL - Ensures
idandgeometrycolumns are present and non-nullable
Datasets¶
Two remote admin boundary datasets are supported:
| Dataset | Standard | Columns Added | Description |
|---|---|---|---|
gaul (default) |
GAUL naming + ISO 3166-1 alpha-3 | admin:continent, admin:country, admin:department |
FAO Global Administrative Unit Layers (GAUL) L2 - worldwide coverage with standardized naming |
overture |
Vecorel compliant (ISO 3166-1/2) | admin:country_code, admin:subdivision_code |
Overture Maps Divisions with ISO 3166 codes (219 countries + 53 dependent territories, 3,544 regions) - docs |
Vecorel Compliance (Overture Dataset Only)¶
The overture dataset follows the Vecorel administrative division extension specification with standardized ISO codes:
admin:country_code(REQUIRED): ISO 3166-1 alpha-2 country code (e.g., "US", "AR", "DE")admin:subdivision_code: ISO 3166-2 subdivision code WITHOUT country prefix (e.g., "CA" not "US-CA")
The tool automatically transforms Overture's native region codes (e.g., "US-CA") to strip the country prefix for Vecorel compliance.
Note: The GAUL dataset uses FAO's standardized naming system but is NOT Vecorel compliant:
- Has ISO 3166-1 alpha-3 codes (e.g., "TZA"), but Vecorel requires alpha-2 (e.g., "TZ")
- Uses GAUL's standardized naming for subnational units, not ISO 3166-2 codes
- Columns: admin:continent (continent name), admin:country (GAUL country name), admin:department (GAUL L2 name)
Notes¶
- Overture dataset: Vecorel compliant with ISO 3166-1 alpha-2 and ISO 3166-2 codes
- GAUL dataset: FAO standardized naming system - source.coop GAUL L2
- Performs spatial intersection to assign admin divisions based on geometry
- Requires internet connection to access remote datasets
- Uses spatial extent filtering and bbox columns for optimization
Overture: per-level cache and simplified boundaries
The Overture dataset uses a per-level cache: country and region boundaries are downloaded and cached as separate files rather than one combined file.
The country level covers dependent territories as well as sovereign
states, each attributed to its own ISO 3166-1 alpha-2 code — GF for
French Guiana, PR for Puerto Rico, RE for Réunion, GL, HK, NC
and the rest, plus BQ and SJ for the Caribbean and Arctic islands
Overture files under placeholder codes. They are not folded into their
sovereign's code.
Overture boundaries are simplified with ST_SimplifyPreserveTopology at a
tolerance of 0.0001 degrees (roughly 11 m near the equator, ~7 m at
50°N). Country and region layers are simplified independently, so
shared borders are not perfectly coincident. As a result, near-border
attribution is approximate: a feature sitting in a border zone may be
attributed to the neighboring region. This is a deliberate
accuracy-for-memory tradeoff. If you need exact boundaries near borders,
use the gaul dataset instead.
Caching¶
Admin datasets (GAUL, Overture) are automatically cached locally on first use:
- First run: Downloads and caches the full dataset (~5-50MB depending on dataset)
- Subsequent runs: Uses cached version (instant startup)
- Cache location:
~/.geoparquet-io/cache/admin/ - Warning: Shown if cache is older than 6 months
Cache management options:
# Skip cache and use remote directly
gpio add admin-divisions input.parquet output.parquet --dataset gaul --no-cache
# Clear all cached datasets (prompts for confirmation)
gpio add admin-divisions input.parquet output.parquet --dataset gaul --clear-cache
import geoparquet_io as gpio
from geoparquet_io.core.admin_datasets import get_cache_dir, clear_cache
# Default: uses cached datasets automatically
gpio.read('input.parquet').add_admin_divisions(
dataset='overture',
levels=['country', 'region']
).write('output.parquet')
# Check cache location (~/.geoparquet-io/cache/admin/)
cache_dir = get_cache_dir()
print(f"Cache location: {cache_dir}")
# Clear all cached admin datasets
result = clear_cache(confirm=True)
print(f"Cleared {result['files_deleted']} files, {result['bytes_freed'] / 1024 / 1024:.2f} MB freed")
When to clear cache
Clear your cache when you need fresh admin boundary data, such as after a
new Overture Maps release. The cache files are named with version numbers
(e.g., gaul-2024-12-19.parquet, overture-2025-10-22.0.parquet).
Common Options¶
All add commands support:
# Compression settings
--compression [ZSTD|GZIP|BROTLI|LZ4|SNAPPY|UNCOMPRESSED]
--compression-level [1-22]
# Row group sizing
--row-group-size [exact row count]
--row-group-size-mb [target size like '256MB' or '1GB']
# Workflow options
--dry-run # Preview SQL without executing
--verbose # Detailed output
--preview # Preview results (partition commands)
--hive # Use Hive-style partitioning
--overwrite # Overwrite existing files
--aws-profile NAME # AWS profile for S3 operations
--add-bbox # Auto-add bbox if missing (some commands)
See Also¶
- CLI Reference - Full command reference
- partition command
- sort command