Coordinate Precision Reduction for Smaller GeoParquet

Snap every ordinate to a grid matched to the data’s real positional accuracy, and a geometry column typically loses 40 to 60 per cent of its compressed size — with a bounded, documented, and uniform positional error. The saving is not from storing fewer bytes per coordinate; a float64 is always eight bytes. It is from making the low-order mantissa bits identical across the column, so the compressor collapses what was previously incompressible noise. This page is the operational procedure behind geometry encoding and coordinate precision.

Quick Reference

Grid Ground error Typical geometry-column saving Primary Use Case
1 mm 1 mm ~18% Engineering as-built, control networks
1 cm 1 cm 40–45% Cadastral survey, utility connections
10 cm 10 cm 55–60% Asset inventories, RTK-derived tracks
1 m 1 m 65–70% Administrative boundaries, land cover
10 m 10 m 72–75% Generalised thematic layers only

Why the Grid, Not the Type

A compressor is a predictor. It shrinks data by noticing that a byte is likely given the bytes around it, and it cannot shrink data it cannot predict. In a column of float64 coordinates spanning one country, the sign bit, the exponent, and the top of the mantissa are nearly constant — highly predictable, heavily compressed. The bottom three bytes of every ordinate are effectively uniform random, and uniform random data compresses to itself.

Rounding to a grid sets those bits to a fixed pattern. The column is the same width, every value is still a float64, and the compressed column is dramatically smaller because the unpredictable part has been removed. This is why the saving only appears after compression, and why teams who measure the raw column before and after conclude that nothing happened.

Where precision reduction stops paying A curve of compressed geometry column size against quantization grid size, from one millimetre through ten metres. Size falls steeply between one millimetre and ten centimetres, where the random low mantissa bits are being removed, then flattens after about one metre because the remaining bits encode real position rather than noise. A shaded band marks the range from one centimetre to one metre where most of the saving is available at bounded error, and two annotations mark that finer grids buy little and coarser grids cost accuracy without buying much space. Compressed geometry column, 4.2 M polygons, ZSTD level 3 the useful band 1 mm 1 cm 10 cm 1 m 10 m quantization grid (log scale) → GB compressed → finer buys little coarser costs accuracy

The Procedure

python
# Requires: shapely>=2.0, geopandas>=1.0, numpy>=1.26  (Python 3.10+)
from __future__ import annotations

from dataclasses import dataclass

import geopandas as gpd
import numpy as np
from shapely import get_coordinates, set_precision
from shapely.errors import GEOSException

METRES_PER_DEGREE = 111_320.0


@dataclass(frozen=True)
class ReductionReport:
    grid_crs_units: float
    accuracy_m: float
    features: int
    collapsed: int
    invalid_added: int
    max_shift_m: float
    p99_shift_m: float

    def summary(self) -> str:
        return (
            f"grid {self.grid_crs_units:g} · max shift {self.max_shift_m:.3f} m · "
            f"p99 {self.p99_shift_m:.3f} m · {self.collapsed} collapsed · "
            f"{self.invalid_added} newly invalid"
        )


def reduce_precision(
    frame: gpd.GeoDataFrame,
    accuracy_m: float,
    *,
    max_collapse_fraction: float = 0.0,
) -> tuple[gpd.GeoDataFrame, ReductionReport]:
    """Snap to an absolute grid and prove the result is still the same data."""
    if frame.crs is None:
        raise ValueError("a CRS is required — grids are expressed in CRS units")
    if frame.empty:
        raise ValueError("nothing to reduce")

    geographic = bool(frame.crs.is_geographic)
    grid = accuracy_m / METRES_PER_DEGREE if geographic else accuracy_m
    scale = METRES_PER_DEGREE if geographic else 1.0

    before = frame.geometry.to_numpy()
    invalid_before = int((~frame.geometry.is_valid).sum())

    try:
        # An absolute grid anchored at the CRS origin — identical for every
        # geometry, so two features that shared a vertex still share it.
        after = set_precision(before, grid_size=grid)
    except GEOSException as exc:
        raise RuntimeError(f"precision reduction failed at grid {grid}: {exc}") from exc

    out = frame.copy()
    out.geometry = after

    collapsed = int(sum(1 for g in after if g is None or g.is_empty))
    if collapsed > max_collapse_fraction * len(frame):
        raise RuntimeError(
            f"{collapsed} feature(s) collapsed at grid {grid} — smaller than one "
            f"grid cell. Use a finer grid or filter them deliberately."
        )

    invalid_added = int((~out.geometry.is_valid).sum()) - invalid_before
    if invalid_added > 0:
        raise RuntimeError(f"grid {grid} introduced {invalid_added} invalid geometries")

    shifts = []
    for old, new in zip(before, after):
        if old is None or new is None or new.is_empty:
            continue
        a, b = get_coordinates(old), get_coordinates(new)
        shifts.append(grid if a.shape != b.shape else float(np.abs(a - b).max()))
    arr = (np.asarray(shifts) if shifts else np.zeros(1)) * scale

    return out, ReductionReport(
        grid_crs_units=grid,
        accuracy_m=accuracy_m,
        features=len(frame),
        collapsed=collapsed,
        invalid_added=max(invalid_added, 0),
        max_shift_m=float(arr.max()),
        p99_shift_m=float(np.percentile(arr, 99)),
    )

Validation

Two things must be true after reduction: the geometry is still valid, and the positional error is inside the budget you declared. Both are cheap to assert and neither is optional.

python
# Requires: geopandas>=1.0, shapely>=2.0 — prove the reduction was within budget
import geopandas as gpd
from shapely import get_coordinates
import numpy as np

original = gpd.read_parquet("parcels.full.parquet")
reduced = gpd.read_parquet("parcels.10cm.parquet")

assert len(original) == len(reduced), "feature count changed — something collapsed"
assert reduced.geometry.is_valid.all(), "invalid geometry after reduction"

shift = max(
    float(np.abs(get_coordinates(a) - get_coordinates(b)).max())
    for a, b in zip(original.geometry, reduced.geometry)
    if a is not None and b is not None
    and get_coordinates(a).shape == get_coordinates(b).shape
)
print(f"max positional shift: {shift * 111_320:.3f} m")   # expect ≤ the declared grid

Expected results at a 10 cm grid on a metre-based CRS: maximum shift at or just under 0.10 m, p99 shift around 0.06 m, zero collapsed features on any layer whose smallest feature exceeds a metre, and a compressed geometry column 55–60% smaller.

Why a shared absolute grid preserves shared boundaries On the left, two adjacent parcels drawn at full precision with a common edge; the grid is shown faintly behind them. In the middle, both are snapped against the same absolute grid: every shared vertex rounds to the same grid intersection, so the common edge remains exactly shared and no sliver appears. On the right, the failure mode is shown: the two parcels snapped against grids with different origins, so their shared vertices round to different points and a thin sliver gap opens between them. Full precision one shared edge two vertices at identical coordinates Shared absolute grid edge still exactly shared both vertices round to the same intersection Different grid origins a sliver opens along the edge every downstream overlay inherits it Grid size and origin are a dataset-family decision, not a per-file one. Reduce precision in the CRS you intend to store, as the last step Two pipelines. In the wrong order, coordinates are snapped in the source CRS and then reprojected, so the regular grid becomes an irregular latitude-dependent spacing and the accuracy guarantee no longer holds. In the right order, the data is reprojected first and snapped last, so the grid is regular in the CRS the file actually stores and the stated maximum displacement is true. Wrong order source CRS full precision snap to grid regular here reproject grid becomes irregular — the guarantee is void Right order source CRS full precision reproject target CRS snap to grid, then write regular in the CRS the file stores Snapping is the last step before the write, every time.

Edge Cases and Caveats

Features smaller than one grid cell. A 60 cm inspection chamber on a 1 m grid becomes a point or disappears. The implementation refuses to proceed by default rather than silently dropping features; if some collapse is genuinely acceptable, raise the threshold explicitly so the decision is recorded in code review rather than in a log line nobody read.

Snapping between projections. Reduce precision in the CRS you intend to store, as the last step before writing. A grid applied before a re-projection becomes an irregular, latitude-dependent spacing afterwards, which defeats both the size argument and the accuracy guarantee.

Layers in a family reduced at different grids. Parcels at 1 cm and the roads that bound them at 1 m will not share edges, and every overlay produces slivers. Record the grid in the dataset metadata alongside the CRS, the way metadata preservation records the projection, so a later reader can check compatibility before joining.

Frequently Asked Questions

How much smaller does a geometry column actually get?

On a typical national polygon layer, 40 to 45 per cent at a one-centimetre grid and 55 to 60 per cent at ten centimetres, measured after ZSTD level 3. Point layers gain less because they carry fewer ordinates per feature relative to their attributes, and dense coastline or contour data gains more. The saving comes entirely from making the low mantissa bits constant so the entropy coder can collapse them.

What grid should I use if I do not know the data’s accuracy?

Find out before you snap. That said, there is a safe diagnostic: compute the distribution of distances between consecutive vertices. If the smallest gaps cluster around a value, that value is close to the digitising resolution and a grid at or below it is safe. If they extend smoothly down to sub-millimetre, the data has been through a re-projection and the tail is arithmetic noise rather than measurement.

Can precision reduction be undone?

No. The discarded bits are gone, and no later processing recovers them. This is why the recommended pattern is to keep one archival copy of survey-grade source data at full precision and apply reduction only to derived delivery copies. Storage for a single master is cheap relative to the cost of discovering, months later, that a boundary dataset was quantized past what a legal process requires.


← Back to Geometry Encoding and Coordinate Precision