geodataframe-to-pmtiles
Write PMTiles vector archives from one or more GeoPandas GeoDataFrames using GDAL directly — no subprocesses, no temporary files.
Install
Install the latest release from PyPI:
pip install geodataframe-to-pmtiles
To install from a source checkout instead:
git clone https://github.com/palewire/geodataframe-to-pmtiles.git
cd geodataframe-to-pmtiles
uv sync
Note: The library itself is pure Python, but
gpm.write()needs a native GDAL runtime with the PMTiles driver available. The package imports without GDAL; callinggpm.write()without it raises a clearRuntimeError. In CI we install GDAL from conda-forge. Locally, install GDAL separately via conda-forge, Homebrew, or your operating system package manager before writing PMTiles archives.
Diagnose GDAL
After installing GDAL, check that its Python bindings, native library, PMTiles driver, and a real in-memory write all work:
python -m geodataframe_to_pmtiles check
python -m geodataframe_to_pmtiles check --json
--json prints a small, stable report that is safe to include in a bug report.
The check runs only when requested; it is not an install hook. Conda-forge is
the most reliable setup on every platform. On macOS, install GDAL with Homebrew
and use Python bindings built for that installation. On Linux, install matching
GDAL runtime and Python packages from the same system package source.
Usage
import geopandas as gpd
from pathlib import Path
import geodataframe_to_pmtiles as gpm
# Any explicit CRS is accepted — reprojection to EPSG:4326 is automatic.
points = gpd.read_file("points.geojson")
polygons = gpd.read_file("polys.geojson")
gpm.write(
{"points": points, "polygons": polygons},
Path("output.pmtiles"),
min_zoom=0,
max_zoom=8,
name="my map",
description="Points and polygons",
on_overflow="error", # default: reject reported tile-level data loss
attribution="© OpenStreetMap contributors", # optional; stored in TileJSON metadata
)
GeoDataFrames passed to gpm.write() must already carry a CRS. If your
source format does not store CRS metadata, set one before writing:
points = points.set_crs("EPSG:4326")
polygons = polygons.set_crs("EPSG:4326")
Write to a BytesIO stream instead of a file:
import io
buf = io.BytesIO()
gpm.write({"points": points}, buf)
Write a single GeoDataFrame with an explicit layer name:
import geodataframe_to_pmtiles as gpm
gpm.write(points, Path("output.pmtiles"), layer="points")
Test coverage
The test suite includes semantic conformance checks that write tracked climate
and Tippecanoe fixtures through GDAL, then decode the resulting PMTiles
archives with the official pmtiles reader and mapbox-vector-tile. The tests
assert header metadata, source-layer names, property schemas, hole
preservation, and feature order while ignoring raw bytes and protobuf ordering.
Documentation
The concise, single-page documentation is built from
docs/index.md and published at
https://palewi.re/docs/geodataframe-to-pmtiles/.
gpm.write — two call forms
Mapping form — multiple named layers:
gpm.write({"name": gdf, ...}, output, *, min_zoom, max_zoom, ...)
Single-frame form — one layer with an explicit name:
gpm.write(gdf, output, *, layer="name", min_zoom, max_zoom, ...)
| Parameter | Type | Default | Description |
|---|---|---|---|
layers |
Mapping[str, GeoDataFrame] or GeoDataFrame |
required | Layer name → GeoDataFrame mapping (mapping form), or a single GeoDataFrame (single-frame form, requires layer). Any explicit CRS accepted; non-EPSG:4326 layers are auto-reprojected. Inputs must still carry a CRS and are not mutated. |
output |
str | Path | BinaryIO |
required | Destination file path (string or Path) or binary stream. |
layer |
str |
(omit for mapping) | Non-empty layer name. Required for the single-frame form; must be omitted entirely when layers is a mapping. |
min_zoom |
int |
0 |
Archive-wide minimum zoom level (0-22). |
max_zoom |
int |
8 |
Archive-wide maximum zoom level (0-22). |
name |
str |
"" |
Tileset name stored in archive metadata. |
description |
str |
"" |
Human-readable description in archive metadata. |
attribution |
str |
"" |
Attribution string stored in TileJSON metadata under "attribution". Unicode and HTML preserved. Omit or pass "" to skip. |
json_fields |
Collection[str] | None |
None |
Columns to JSON-encode (list/dict values). None auto-encodes all; explicit set restricts to named columns only. |
on_overflow |
"error" | "unsafe" |
"error" |
Reject detected GDAL tile-limit actions, or explicitly accept them. |
simplification |
float | None |
None |
Geometry simplification tolerance (tile units). None = disabled. |
Property normalisation
| Python / pandas type | MVT field | Notes |
|---|---|---|
str |
String | |
bool / np.bool_ |
Boolean | Native MVT boolean |
int / np.integer |
Integer64 | |
float / np.float_ |
Real | NaN → null |
datetime |
String | ISO 8601 |
list / dict |
String | JSON-encoded; column must be in json_fields or json_fields=None (auto) |
None / pd.NA |
null | |
| other | — | UnsupportedPropertyTypeError |
Boolean columns may contain nulls, including pandas BooleanDtype values even
if every value is null. They must not mix booleans with numeric 0 or 1:
those are integers and remain numeric. Mixed scalar boolean/non-boolean
columns raise UnsupportedPropertyTypeError instead of silently changing
values.
Exceptions
| Exception | When raised |
|---|---|
EmptyLayerError |
layers is empty or a GDF has no features. |
MissingCRSError |
A GDF has no CRS set (explicit source CRS required). |
UnsupportedCRSError |
A GDF's CRS definition cannot be resolved by the installed stack (chained from root cause). |
CRSTransformError |
Coordinate transformation to EPSG:4326 failed at runtime (chained from root cause). |
UnsupportedPropertyTypeError |
A column has an unrecognised type, or a list/dict column not in json_fields. |
TileOverflowError |
GDAL reported a feature-cap rebuild or size-driven geometry recode. The destination is unchanged. |
Overflow policy
GDAL's MVT encoder can drop features after MAX_FEATURES is reached and
reduce geometry precision after MAX_SIZE is exceeded. The writer uses
practical limits (300,000 features and 10 MB per tile) and captures the
encoder's diagnostics during finalization. The default
on_overflow="error" raises TileOverflowError and leaves the Path or
stream untouched when either action occurs. Its violations describe the
limit, configured value, observed value, and tile coordinate when GDAL reports
one.
The 200,001-feature z0 spike remains supported and is independently decoded in the test suite. This is not a capacity promise: a clustered layer or dense geometry can still exceed a tile limit, but it cannot be published through the default API after GDAL reports that action.
on_overflow="unsafe" is an explicit opt-out. It emits a warning and may
publish an archive with missing features or lower-precision geometry.
Climate-monitor guidance
Use the default policy for climate cell layers and treat TileOverflowError as
a signal to split the layer or lower its density at the affected zoom. Do not
use on_overflow="unsafe" for maps where holes or coordinate changes would
alter reported conditions.
Known limitations
- Feature count inflation when reading back: MVT stores features in every intersecting tile; read-back counts exceed input counts. This is not data loss.
- Simplification disabled by default: pass
simplification=<float>to enable.
Development
make install
make check # lint, format, type checks
make verify # full suite: checks, tests, build, docs
See AGENTS.md and CONTRIBUTING.md.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file geodataframe_to_pmtiles-0.2.0.tar.gz.
File metadata
- Download URL: geodataframe_to_pmtiles-0.2.0.tar.gz
- Upload date:
- Size: 35.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8b2d5e689249c07789a963886014f8ba24e072743160cd1a9c32caae5dd21d9a
|
|
| MD5 |
439514c49ffba7c5069d5e66161806a6
|
|
| BLAKE2b-256 |
8ee20161b70622dbe6c24c617d07769f68c12e549cef7d65696d44074d391035
|
Provenance
The following attestation bundles were made for geodataframe_to_pmtiles-0.2.0.tar.gz:
Publisher:
continuous-deployment.yaml on palewire/geodataframe-to-pmtiles
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
geodataframe_to_pmtiles-0.2.0.tar.gz -
Subject digest:
8b2d5e689249c07789a963886014f8ba24e072743160cd1a9c32caae5dd21d9a - Sigstore transparency entry: 2565422323
- Sigstore integration time:
-
Permalink:
palewire/geodataframe-to-pmtiles@deb5598d1e466b367571ae22cd26a57de9eb800c -
Branch / Tag:
refs/tags/0.2.0 - Owner: https://github.com/palewire
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
continuous-deployment.yaml@deb5598d1e466b367571ae22cd26a57de9eb800c -
Trigger Event:
push
-
Statement type:
File details
Details for the file geodataframe_to_pmtiles-0.2.0-py3-none-any.whl.
File metadata
- Download URL: geodataframe_to_pmtiles-0.2.0-py3-none-any.whl
- Upload date:
- Size: 30.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
95d709a16ca9bc2228360b1ddb082c16c269fbc1707ba467e56fa77269ee2d7d
|
|
| MD5 |
b21fbf5ed5a5e636c11b4b118080f458
|
|
| BLAKE2b-256 |
60c4e6164b8fbe4b08d33ef4b54dbf650918b6ba50a1540427665e85b13b08ee
|
Provenance
The following attestation bundles were made for geodataframe_to_pmtiles-0.2.0-py3-none-any.whl:
Publisher:
continuous-deployment.yaml on palewire/geodataframe-to-pmtiles
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
geodataframe_to_pmtiles-0.2.0-py3-none-any.whl -
Subject digest:
95d709a16ca9bc2228360b1ddb082c16c269fbc1707ba467e56fa77269ee2d7d - Sigstore transparency entry: 2565422437
- Sigstore integration time:
-
Permalink:
palewire/geodataframe-to-pmtiles@deb5598d1e466b367571ae22cd26a57de9eb800c -
Branch / Tag:
refs/tags/0.2.0 - Owner: https://github.com/palewire
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
continuous-deployment.yaml@deb5598d1e466b367571ae22cd26a57de9eb800c -
Trigger Event:
push
-
Statement type: