Coordinate Reference Systems¶
A Coordinate Reference System (CRS) describes how the numeric coordinates in a geometry, geography, or raster relate to locations on the Earth. Two datasets whose coordinates look similar are only comparable if they share a CRS, so SedonaDB tracks a CRS alongside every spatial value and uses it to decide whether an operation is well defined.
This page explains how SedonaDB represents a CRS, how it decides whether two CRSes are equal, and what that means when you combine data from different sources.
The three forms of a CRS¶
A CRS definition attached to a spatial value can take one of three forms. SedonaDB recognizes and stores whichever form you provide:
- An authority code — a compact
AUTHORITY:CODEstring such asEPSG:4326,EPSG:3857,ESRI:102005, orOGC:CRS84. This is the most common and most portable form. A bare numeric code like4269is accepted and interpreted asEPSG:4269. - PROJJSON — a JSON object following the PROJJSON schema. This is the fully self-describing form used when writing GeoParquet and Iceberg.
- A WKT string — a WKT1 or WKT2 CRS definition (
PROJCRS[...],GEOGCRS[...],PROJCS[...], and similar). This is the form most often found in GeoTIFF and other GDAL-sourced files.
You choose the form when you set a CRS (for example with ST_SetCRS /
RS_SetCRS), and files you read carry whichever form they were written with.
The definition is preserved verbatim¶
SedonaDB does not rewrite a CRS you give it. Whatever form you supply is the form stored and the form you get back:
-- Set as an authority code, read back as that same authority code
SELECT ST_CRS(ST_SetCRS(ST_Point(0.25, 0.25), 'EPSG:3857'));
-- => EPSG:3857
A WKT or PROJJSON definition is kept in full — SedonaDB will not collapse it
down to an embedded AUTHORITY:CODE even when one is present. Nothing is
discarded on the way in, so a consumer that wants the compact form can ask for
it, but one that needs the full definition still has it.
Internally there are two views of the stored definition:
- the round-trippable / PROJ-consumable form (what
ST_CRSandRS_CRSreturn, and what is handed to PROJ at transform time), and - the field-metadata form (what is embedded in GeoArrow / GeoParquet metadata).
Both preserve the original authority code, PROJJSON, or WKT as given. The
only canonicalization SedonaDB applies is for the longitude/latitude WGS84
aliases: EPSG:4326 and OGC:CRS84 describe the same datum but imply different
axis orders, so in the field-metadata form both are written as OGC:CRS84 to
keep the axis order (longitude, latitude) explicit for downstream readers. More
generally, SedonaDB always assumes longitude, latitude axis order regardless of
what a CRS declares — many geographic CRSes, EPSG:4326 included, officially
specify latitude first, but coordinates are longitude-first everywhere and
ST_Transform normalizes every transform to that order. The PROJ-consumable
form still returns the authority code exactly as you set it.
Column-level and row-level CRS¶
For vector data — geometry and geography — a CRS can attach at two levels.
A column-level CRS is a property of the whole column: the column carries a
single CRS in its type (and, on disk, in its field metadata), and every value
in the column shares it.
This is the form ST_CRS and ST_SRID read, the form the equality rules below
compare, and the one ST_SetCRS sets.
A row-level CRS instead travels with each value, which carries its own CRS rather than inheriting one from the column. Internally this is the value paired with a per-row CRS string.
Most vector functions accept either form. For a row-level input, SedonaDB
applies the CRS rules per value: a function whose result does not depend on the
CRS (such as ST_Area) simply uses the geometry; a function taking two or more
geometries requires their CRSes to be compatible; and a function that returns a
geometry carries the CRS through to its result. The important difference from a
column-level CRS is when compatibility is enforced — a column-level mismatch
is caught at query-planning time (see below), whereas a row-level CRS is not
known until the data flows, so it is reconciled while the query runs.
Rasters do not have a column-level CRS. A raster carries its own CRS as part
of the raster value, so every raster in a column is self-describing; RS_CRS /
RS_SRID read the CRS from each raster and RS_SetCRS sets it on individual
rasters.
Equality: how SedonaDB compares two CRSes¶
Deciding whether two CRSes are "the same" is the subtle part, and SedonaDB's rule is deliberately simple and conservative:
- If both CRSes expose an authority code, the authority codes are compared.
The issuing authority is treated as the source of truth about identity, so
EPSG:3857equalsEPSG:3857regardless of how each side was originally spelled. (TheEPSG:4326/OGC:CRS84lon/lat pair is the one built-in alias that compares equal.) - Otherwise, SedonaDB falls back to a structural / string comparison of the definitions — matching PROJJSON objects structurally, or comparing WKT strings directly.
This comparison is intentionally lenient: it can false-negative (report two definitions as different when a full geodetic analysis would call them equivalent), but it will never false-positive (it never claims two genuinely different CRSes are the same). SedonaDB keeps the PROJ library out of the core schema layer entirely, so there is no semantic-equivalence check here. This is a different trade-off from, for example, GeoPandas, which compares CRSes through PROJ using an approximate, confidence-based similarity threshold.
What this means for you¶
Vector operations that require their inputs to share a CRS — spatial joins,
ST_Intersects, distance predicates, and so on — raise an error on a
mismatch rather than silently reprojecting one side to match the other.
Silently reprojecting geometry coordinates is exactly the kind of hidden,
easy-to-miss behavior that produces wrong answers, so SedonaDB refuses to guess:
Error during planning: Mismatched CRS arguments: epsg:3857 vs epsg:4326
Use ST_Transform() or ST_SetSRID() to ensure arguments are compatible.
To fix a mismatch, make the CRSes agree before the operation:
- If the coordinates are in different systems, reproject one side with
ST_Transform(which actually moves the coordinates). - If a value is simply missing a CRS, or is labeled incorrectly, attach the
right one with
ST_SetCRS/ST_SetSRID(metadata only — no coordinates change).
Because equality can false-negative, two definitions that are equivalent but
are spelled differently (say, an authority code on one side and an
authority-less WKT on the other) may be reported as mismatched. The fix is the
same: normalize both sides to the same definition with ST_Transform or
ST_SetCRS.
RS_Intersects has different behaviour than ST_Intersects¶
A spatial join involving a raster tests the raster's footprint (the polygon through its corners), not its pixels. When a raster is joined against a geometry, SedonaDB reconciles the two CRSes as follows:
- They share a CRS, or both are CRS-less — the footprints are compared directly, with no reprojection.
- Both have a CRS but they differ — SedonaDB reprojects the raster's footprint into the geometry's CRS and the join proceeds, rather than erroring as the vector path would. Reprojecting a footprint is far cheaper and safer than resampling the pixel grid.
- Only one side has a CRS — this is an error, just as on the vector path: SedonaDB will not reproject into or out of an unknown frame.
The reprojected case is a close approximation rather than an exact boundary: reprojection is sometimes non-linear, so the footprint's straight edges may bow into curves in the target CRS, and reprojecting only the four corners can under-cover the true extent. SedonaDB handles this by densifying each edge — adding interior points in the raster's own CRS, where the edges are still straight — then reprojecting all of them, so the reprojected footprint follows the curve.
One performance note: an accelerated spatial join relies on a spatial index built over a single, column-level CRS. A geometry or raster column that carries a row-level CRS can't use that index, so the join falls back to a slower nested loop join evaluation — prefer a column-level CRS for join-heavy work.
SRID vs CRS¶
An SRID (Spatial Reference Identifier) is just the numeric-code view of a
CRS. ST_SRID / RS_SRID return the integer, and ST_SetSRID / RS_SetSRID
set a CRS from one. SedonaDB maps between the two following the majority of SRID usage in the wild: SRID 0
means "no CRS", 4326 maps to OGC:CRS84, and any other value N maps to
EPSG:N. A CRS only has an SRID when it carries an EPSG (or lon/lat) authority
code; a purely custom WKT or PROJJSON definition with no authority has no SRID.
Internals / design¶
A few deeper points, for the curious:
- The core schema layer is PROJ-free and GDAL-free. CRS representation,
storage, and equality all live in the
sedona-schemacrate, which has no dependency on PROJ or GDAL. This keeps the low-level crate light and means equality never depends on a native geodetic library being present. - PROJ is used only at transform time. Reprojection (
ST_Transform) is performed by a pluggable CRS engine; the default is a PROJ-backed engine that lives in its own crate and is injected into the session. The stored CRS definition — including verbatim WKT — is handed to PROJ only when a transform actually runs. - A small in-crate WKT parser extracts identity without pulling in PROJ. To
read an authority code, SRID, or ellipsoid parameters out of a WKT definition
(needed for equality and for spherical geography),
sedona-schemauses a tiny, self-contained WKT-node parser rather than linking PROJ. It reads the authority only from the top-levelAUTHORITY[...]/ID[...]tag, so a custom CRS is never misidentified by an authority tag buried on a nested unit or projection parameter. A WKT that is malformed or carries no top-level authority is still stored verbatim — it simply has no derived SRID.
See also¶
- Joining Spatial Data with Different Coordinate Systems — a worked example of the CRS-mismatch error and how to resolve it across a spatial join.