Choosing a Canonical CRS for Spatial LLM Pipelines

Pick one storage frame and a rule for measurement projections, so every geometry in the system is comparable and every metric answer is computed in real units.

Every spatial system converges on one storage frame, whether or not anyone chose it. Choosing deliberately takes an afternoon and settles a decade of arguments about why two distances disagree. This guide works through the decision and the rule that goes with it — one frame for storage, a projection selected per measurement — as the first step of coordinate reference system normalization.

When to Use This Approach

Make the choice before the first geometry is stored, and revisit it only when the system’s geographic scope changes fundamentally. Retrofitting a different canonical frame onto a populated corpus is a migration, not a configuration change.

Situation Storage frame Reason
Multi-country or growing scope Global geographic Survives expansion without migration
Single country, metric-heavy work National projected, or global geographic Either defensible; projected saves transforms
Data arrives in one national grid only That grid, if scope is fixed No transform on the hot path
Web mapping is the primary consumer Global geographic, transform at render Never store the display projection

The last row is the one that causes the most damage. Storing geometry in a web mapping projection is convenient for one consumer and wrong for every measurement: that projection’s distortion grows with latitude and areas computed in it are meaningless.

Three frames with three jobsOne global geographic frame for storage, a projection chosen per operation for measurement, and whatever the map expects for display, with transformations flowing outward from storage.storageone global geographic framemeasurementper operation, metricdisplayat render, never stored
Transformations point outward and never back. The moment a display projection is written back into storage, the storage frame has two definitions and every comparison in the system becomes conditional.

Implementation

The choice is expressed as configuration plus two functions: one that declares the canonical frame, and one that selects a measurement projection for a given geometry and purpose.

import logging
from dataclasses import dataclass
from typing import Literal

from pyproj import CRS, Transformer
from pyproj.exceptions import CRSError

log = logging.getLogger("canonical_crs")

CANONICAL_EPSG = 4326                      # storage: global, unambiguous, portable
EQUAL_AREA_EPSG = 6933                     # global equal-area, for area measurement

Purpose = Literal["area", "distance", "display"]


@dataclass(frozen=True)
class Projection:
    crs: CRS
    why: str                               # recorded with any number it produces


def canonical() -> CRS:
    """The one frame everything is stored and compared in."""
    try:
        return CRS.from_epsg(CANONICAL_EPSG)
    except CRSError as exc:                # a broken installation, not a data problem
        raise RuntimeError(f"canonical frame EPSG:{CANONICAL_EPSG} unavailable: {exc}") from exc


def measurement_projection(geom, purpose: Purpose) -> Projection:
    """Select a metric projection appropriate to this geometry and this question."""
    if purpose == "area":
        return Projection(CRS.from_epsg(EQUAL_AREA_EPSG),
                          f"equal-area EPSG:{EQUAL_AREA_EPSG}")
    if purpose == "distance":
        lon, lat = geom.centroid.x, geom.centroid.y
        proj = CRS.from_proj4(
            f"+proj=aeqd +lat_0={lat:.6f} +lon_0={lon:.6f} "
            "+datum=WGS84 +units=m +no_defs")
        return Projection(proj, f"azimuthal equidistant centred on {lat:.4f}, {lon:.4f}")
    raise ValueError(f"no measurement projection for purpose {purpose!r}")


def to_projection(geom, projection: Projection):
    """Move geometry from the canonical frame into a measurement projection."""
    tf = Transformer.from_crs(canonical(), projection.crs, always_xy=True)
    from shapely.ops import transform
    try:
        return transform(tf.transform, geom)
    except Exception as exc:
        log.warning("projection to %s failed: %s", projection.why, exc)
        raise

Two decisions in that code are worth defending. The measurement projection is built per geometry for distance work, because a projection centred on the thing being measured has negligible distortion nearby and any fixed alternative does not. And the projection carries a why string, because a number without the projection that produced it cannot be reproduced or compared to another number produced differently.

Raising on an unknown purpose rather than falling back to the canonical frame is equally deliberate. A silent fallback would compute areas in square degrees, which is the single error this whole arrangement exists to prevent.

Area error from measuring in the wrong projectionThe same square kilometre measured in an equal-area projection, a web mapping projection and raw degrees, showing that only the first returns a usable figure and the error grows with latitude.One square kilometre at 56 degrees north, measured three waysequal-areaweb mappingraw degrees1.00 km²the answer3.20 km²inflated by latitude0.00016square degreesOnly the middle figure looks like a plausible area, which is what makes it dangerous
The wrong answer that looks right. Square degrees are obviously not an area and get caught; a web mapping projection returns square metres of the correct magnitude and is simply wrong by a factor that varies with latitude.

Validation & Testing

from shapely.geometry import box


def test_area_is_measured_in_an_equal_area_projection():
    # A degree-sized box at two latitudes must not report the same area.
    south = box(-3.0, 10.0, -2.0, 11.0)
    north = box(-3.0, 56.0, -2.0, 57.0)
    a_south = to_projection(south, measurement_projection(south, "area")).area
    a_north = to_projection(north, measurement_projection(north, "area")).area
    assert a_south > a_north * 1.5, "equal-area projection is not being applied"


def test_distance_projection_is_centred_on_the_geometry():
    subject = box(-3.2, 55.9, -3.1, 56.0)
    proj = measurement_projection(subject, "distance")
    assert "aeqd" in proj.crs.to_proj4()
    assert "55.9" in proj.why or "55.95" in proj.why


def test_unknown_purpose_raises_rather_than_defaulting():
    try:
        measurement_projection(box(0, 0, 1, 1), "volume")
    except ValueError:
        return
    raise AssertionError("an unknown purpose must not silently use the storage frame")

The first test is a property rather than a fixture check: a degree box near the equator genuinely covers more ground than one near the pole, and any implementation that reports them as equal has skipped the projection. It needs no reference values and does not break when a library updates its constants.

Run these against the real configuration rather than a test double. The value of this whole arrangement is that one module decides the frames, and a test that stubs that module is testing something else.

Gotchas & Edge Cases

Storing the display projection. Convenient for one consumer and corrupting for every measurement, because that projection’s scale factor varies with latitude. Transform at render, and never write the result back.

A national grid chosen before the scope was known. The system expands into a neighbouring country and half the data is now outside the grid’s area of use, where its accuracy degrades quietly. Prefer a global frame unless the scope is genuinely fixed by something other than current ambition.

Per-geometry projections used for comparison. Two areas measured in two different locally centred projections are not strictly comparable. Use the global equal-area projection for areas, which is the same everywhere, and reserve locally centred projections for distances, where the geometry being measured is the centre.

A national grid outgrown by the corpusA projected national frame serves its own territory well and degrades outside it, so a corpus that expands into neighbouring regions accumulates geometry in the frame’s poorly-behaved margins.inside the area of useat the marginoutsideaccurate to centimetresthe frame was designed for thisaccuracy degradesquietlyno longermeaningfulNothing errors as the corpus expands rightward — the numbers simply stop being right
Area of use is a real boundary. A projected frame is a fit to one region, and geometry stored outside it is not slightly worse but progressively meaningless — with no error at the moment the boundary is crossed.

A canonical frame declared but not enforced. Configuration says one thing and a legacy import path writes another. The column type and check constraint described in the parent topic are what make the declaration structural rather than aspirational.

Assuming the axis order of the canonical frame. The global geographic frame is defined latitude-first in its authoritative definition and is handled longitude-first by most software. Pass always_xy=True consistently, and test it, or half the pipeline will disagree with the other half about which number is which.

Run the property tests on real geometry from the corpus rather than on synthetic boxes wherever you can. Synthetic shapes are axis-aligned and centred conveniently, which is exactly the configuration in which a projection error is smallest; a real parcel at a real latitude exercises the same code with the distortions that actually occur.

It is also worth asserting the axis-order convention explicitly in this test file rather than trusting it. Passing always_xy=True everywhere is the correct habit, and a single call site that omits it produces geometry that is silently transposed — a failure that looks like a data problem and is a configuration one.

Frequently Asked Questions

Is a projected canonical frame ever the better choice?

Yes, when the system genuinely will not leave one region and metric operations dominate every request. Storing in a national grid removes a transform from the hot path and makes distances directly computable, which is a real saving on a high-volume workload. The cost is that expansion becomes a migration, and expansion is easier to predict in hindsight than in advance — so take this route when the constraint is external, such as a regulatory boundary, rather than when it merely seems unlikely to change.

Which equal-area projection should be used globally?

Any of the standard global equal-area options works, and the choice matters far less than using one consistently. What matters is that areas across the corpus are computed in the same projection, so that two figures can be compared, and that the projection is equal-area rather than merely metric — a metric projection that is not equal-area will happily report square metres that are wrong by tens of per cent at the edges of its zone.

How should the choice be recorded?

In one module, as a constant with a comment explaining the reasoning, and in the database schema as a typed geometry column. Both matter: the constant is what application code reads, the column type is what stops a different code path from writing something else. A decision recorded only in a design document is one that will be contradicted by the second person to write an import script.

Does the canonical frame apply to raster data too?

Not usefully. Rasters carry their own grids, and reprojecting them to match a vector canonical frame resamples every pixel, which is a loss and — for categorical data — a corruption. The rule for rasters is the inverse: bring the vector to the raster's frame for each operation, as described in vector-raster hybrid processing, and store rasters as they arrive.

What happens to the choice when a new region is added?

With a global geographic storage frame, nothing — that is the property being bought. With a projected national frame it is a migration: every stored geometry must be transformed, every stored measurement recomputed, and every index rebuilt. Knowing that in advance is what makes the original choice a decision rather than a default, and it is worth writing the migration cost down next to the constant so the trade is visible to whoever inherits it.