You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by pa...@apache.org on 2026/06/18 20:47:00 UTC

(sedona-db) branch main updated: feat(rust/sedona-schema): support WKT1/WKT2 CRS strings in deserialize_crs (#953)

This is an automated email from the ASF dual-hosted git repository.

paleolimbot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git


The following commit(s) were added to refs/heads/main by this push:
     new f95c88665 feat(rust/sedona-schema): support WKT1/WKT2 CRS strings in deserialize_crs (#953)
f95c88665 is described below

commit f95c886655193ab40e0dd19cbf55898c99ac7c52
Author: James Willis <ja...@users.noreply.github.com>
AuthorDate: Thu Jun 18 13:46:55 2026 -0700

    feat(rust/sedona-schema): support WKT1/WKT2 CRS strings in deserialize_crs (#953)
---
 c/sedona-proj/src/st_transform.rs                  |  27 +
 docs/reference/sql/st_crs.qmd                      |   3 +-
 docs/reference/sql/st_setcrs.qmd                   |   3 +
 .../tests/functions/test_raster_functions.py       |  42 ++
 python/sedonadb/tests/functions/test_transforms.py |  61 ++
 rust/sedona-schema/src/crs.rs                      | 616 +++++++++++++++++++--
 6 files changed, 720 insertions(+), 32 deletions(-)

diff --git a/c/sedona-proj/src/st_transform.rs b/c/sedona-proj/src/st_transform.rs
index fb219ec2e..5cce449a8 100644
--- a/c/sedona-proj/src/st_transform.rs
+++ b/c/sedona-proj/src/st_transform.rs
@@ -456,6 +456,33 @@ mod tests {
     const NAD83ZONE6PROJ: &str = "EPSG:2230";
     const NAD83_GEOGRAPHIC: &str = "EPSG:4269";
     const WGS84: &str = "EPSG:4326";
+    // EPSG:3857 as WKT1 (AUTHORITY[...]) and WKT2 (ID[...]), for exercising
+    // both WKT flavors through `deserialize_crs` (the to-CRS argument is
+    // deserialized before being handed to PROJ).
+    const WEB_MERCATOR_WKT1: &str = r#"PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"],AUTHORITY["EPSG","3857"]]"#;
+    const WEB_MERCATOR_WKT2: &str = r#"PROJCRS["WGS 84 / Pseudo-Mercator",BASEGEOGCRS["WGS 84",DATUM["World Geodetic System 1984",ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]]],ID["EPSG",4326]],CONVERSION["Popular Visualisation Pseudo-Mercator",METHOD["Popular Visualisation Pseudo Mercator",ID["EPSG",1024]],PARAMETER["Latitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433]],PARAMETER["Longitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433]],PA [...]
+
+    /// A WKT-defined target CRS (both WKT1 and WKT2) must transform identically
+    /// to its authority code — the consumer-side proof that `deserialize_crs`
+    /// now accepts WKT and feeds the verbatim definition to PROJ. Compares
+    /// output WKB, which is independent of how the result's CRS metadata is
+    /// represented.
+    #[rstest]
+    #[case::wkt1(WEB_MERCATOR_WKT1)]
+    #[case::wkt2(WEB_MERCATOR_WKT2)]
+    fn transform_to_wkt_crs_matches_authority_code(#[case] wkt: &str) {
+        let udf = SedonaScalarUDF::from_impl("st_transform", st_transform_impl());
+        let geometry_input = SedonaType::Wkb(Edges::Planar, lnglat());
+        let tester = ScalarUdfTester::new(
+            udf.into(),
+            vec![geometry_input.clone(), SedonaType::Arrow(DataType::Utf8)],
+        );
+
+        let wkb = create_array(&[None, Some("POINT (79.3871 43.6426)")], &geometry_input);
+        let via_wkt = tester.invoke_array_scalar(wkb.clone(), wkt).unwrap();
+        let via_code = tester.invoke_array_scalar(wkb, "EPSG:3857").unwrap();
+        assert_array_equal(&via_wkt, &via_code);
+    }
 
     #[test]
     fn test_invoke_with_string() {
diff --git a/docs/reference/sql/st_crs.qmd b/docs/reference/sql/st_crs.qmd
index b80bdad89..d1ce152e1 100644
--- a/docs/reference/sql/st_crs.qmd
+++ b/docs/reference/sql/st_crs.qmd
@@ -26,7 +26,8 @@ kernels:
 ## Description
 
 Returns the CRS (Coordinate Reference System) string associated with a geometry
-or geography. This is abbreviated as an authority/code if possible (e.g., `'EPSG:3857'`).
+or geography. Whether it was set as an authority code (e.g. `'EPSG:3857'`),
+PROJJSON, or WKT, the CRS is returned in the same form it was set.
 
 ## Examples
 
diff --git a/docs/reference/sql/st_setcrs.qmd b/docs/reference/sql/st_setcrs.qmd
index c7df45d72..a95226241 100644
--- a/docs/reference/sql/st_setcrs.qmd
+++ b/docs/reference/sql/st_setcrs.qmd
@@ -37,6 +37,9 @@ Sets a CRS to a geometry/geography. This is metadata-only - no coordinates are t
 
 This is different from ST_Transform which actually transforms coordinates.
 
+`target_crs` accepts an authority code (e.g. `'EPSG:4326'`), PROJJSON, or WKT.
+It is stored as given and read back unchanged by `ST_CRS`.
+
 ## Examples
 
 ```sql
diff --git a/python/sedonadb/tests/functions/test_raster_functions.py b/python/sedonadb/tests/functions/test_raster_functions.py
index 7412f613c..590d15229 100644
--- a/python/sedonadb/tests/functions/test_raster_functions.py
+++ b/python/sedonadb/tests/functions/test_raster_functions.py
@@ -41,6 +41,48 @@ def test_rs_function(expr, expected):
     eng.assert_query_result(f"SELECT {expr}", expected)
 
 
+# EPSG:3857 as WKT (carries an embedded EPSG authority) and a bespoke Lambert
+# Conformal Conic WKT with no authority code anywhere.
+WKT_3857 = (
+    'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",'
+    'SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],'
+    'AUTHORITY["EPSG","6326"]],AUTHORITY["EPSG","4326"]],'
+    'PROJECTION["Mercator_1SP"],AUTHORITY["EPSG","3857"]]'
+)
+WKT_LCC_NO_AUTHORITY = (
+    'PROJCS["Custom LCC",GEOGCS["WGS 84",DATUM["WGS_1984",'
+    'SPHEROID["WGS 84",6378137,298.257223563]]],'
+    'PROJECTION["Lambert_Conformal_Conic_2SP"],'
+    'PARAMETER["standard_parallel_1",33],PARAMETER["standard_parallel_2",45],'
+    'PARAMETER["latitude_of_origin",39],PARAMETER["central_meridian",-96],'
+    'UNIT["metre",1]]'
+)
+
+
+# WKT1/WKT2 CRS strings round-trip through RS_SetCRS/RS_CRS unchanged, whether or
+# not they carry an embedded authority.
+@pytest.mark.parametrize("wkt", [WKT_3857, WKT_LCC_NO_AUTHORITY])
+def test_rs_setcrs_wkt_roundtrips(wkt):
+    eng = SedonaDB()
+    eng.assert_query_result(f"SELECT RS_CRS(RS_SetCRS(RS_Example(), '{wkt}'))", wkt)
+
+
+def test_rs_srid_from_wkt():
+    """A WKT carrying an EPSG authority resolves to that SRID."""
+    eng = SedonaDB()
+    eng.assert_query_result(
+        f"SELECT RS_SRID(RS_SetCRS(RS_Example(), '{WKT_3857}'))", 3857
+    )
+
+
+def test_rs_srid_from_authorityless_wkt_errors(con):
+    """A WKT with no authority code anywhere has no SRID to extract."""
+    with pytest.raises(Exception, match="SRID"):
+        con.sql(
+            f"SELECT RS_SRID(RS_SetCRS(RS_Example(), '{WKT_LCC_NO_AUTHORITY}'))"
+        ).to_arrow_table()
+
+
 def test_rs_ensureloaded(con, sedona_testing):
     path = sedona_testing / "data/raster/sentinel2.tif"
     t = con.sql("SELECT RS_FromPath($1) AS raster", params=(str(path),))
diff --git a/python/sedonadb/tests/functions/test_transforms.py b/python/sedonadb/tests/functions/test_transforms.py
index 652b5c0a3..3c00241bb 100644
--- a/python/sedonadb/tests/functions/test_transforms.py
+++ b/python/sedonadb/tests/functions/test_transforms.py
@@ -200,6 +200,67 @@ def test_st_crs_sedonadb(eng):
     )
 
 
+# EPSG:3857 as WKT (carries an embedded EPSG authority) and a bespoke Lambert
+# Conformal Conic WKT with no authority code anywhere.
+WKT_3857 = (
+    'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",'
+    'SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],'
+    'AUTHORITY["EPSG","6326"]],AUTHORITY["EPSG","4326"]],'
+    'PROJECTION["Mercator_1SP"],AUTHORITY["EPSG","3857"]]'
+)
+WKT_LCC_NO_AUTHORITY = (
+    'PROJCS["Custom LCC",GEOGCS["WGS 84",DATUM["WGS_1984",'
+    'SPHEROID["WGS 84",6378137,298.257223563]]],'
+    'PROJECTION["Lambert_Conformal_Conic_2SP"],'
+    'PARAMETER["standard_parallel_1",33],PARAMETER["standard_parallel_2",45],'
+    'PARAMETER["latitude_of_origin",39],PARAMETER["central_meridian",-96],'
+    'UNIT["metre",1]]'
+)
+
+
+# WKT1/WKT2 CRS strings round-trip through ST_SetCrs/ST_Crs unchanged, whether
+# or not they carry an embedded authority. (ST_SetCrs/ST_Crs are SedonaDB-only.)
+@pytest.mark.parametrize("wkt", [WKT_3857, WKT_LCC_NO_AUTHORITY])
+def test_st_crs_wkt_roundtrips(con, wkt):
+    result = con.sql(
+        "SELECT ST_Crs(ST_SetCrs(ST_GeomFromText('POINT (0 1)'), $1)) AS crs",
+        params=(wkt,),
+    ).to_arrow_table()
+    assert result["crs"][0].as_py() == wkt
+
+
+def test_st_srid_from_wkt(con):
+    """A WKT carrying an EPSG authority resolves to that SRID."""
+    result = con.sql(
+        "SELECT ST_SRID(ST_SetCrs(ST_GeomFromText('POINT (0 1)'), $1)) AS srid",
+        params=(WKT_3857,),
+    ).to_arrow_table()
+    assert result["srid"][0].as_py() == 3857
+
+
+def test_st_srid_from_authorityless_wkt_errors(con):
+    """A WKT with no authority code anywhere has no SRID to extract."""
+    with pytest.raises(Exception, match="SRID"):
+        con.sql(
+            "SELECT ST_SRID(ST_SetCrs(ST_GeomFromText('POINT (0 1)'), $1))",
+            params=(WKT_LCC_NO_AUTHORITY,),
+        ).to_arrow_table()
+
+
+def test_st_transform_from_wkt_crs(con):
+    """A WKT CRS feeds the PROJ transform like any other CRS string: a point at
+    the EPSG:3857 coordinates of (lon 1, lat 1) transforms back to ~POINT (1 1)."""
+    df = con.sql(
+        "SELECT ST_Transform("
+        "ST_SetCrs(ST_GeomFromText('POINT (111319.490793274 111325.142866385)'), $1),"
+        " 'EPSG:4326') AS geom",
+        params=(WKT_3857,),
+    ).to_pandas()
+    point = df.geometry[0]
+    assert point.x == pytest.approx(1.0, abs=1e-6)
+    assert point.y == pytest.approx(1.0, abs=1e-6)
+
+
 @pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
 @pytest.mark.parametrize(
     ("geom", "dx", "dy", "expected"),
diff --git a/rust/sedona-schema/src/crs.rs b/rust/sedona-schema/src/crs.rs
index 7fbb5da9a..0183d59f9 100644
--- a/rust/sedona-schema/src/crs.rs
+++ b/rust/sedona-schema/src/crs.rs
@@ -38,8 +38,12 @@ thread_local! {
 
 /// Deserialize a specific GeoArrow "crs" value
 ///
-/// Currently the only CRS types supported are PROJJSON and authority:code
-/// where the authority and code are well-known lon/lat (WGS84) codes.
+/// Recognizes three encodings: an `authority:code` string (e.g. `"EPSG:4326"`),
+/// a PROJJSON object, and a WKT1/WKT2 CRS string (e.g. `PROJCS[...]` /
+/// `PROJCRS[...]`). WKT is carried verbatim (so it can be handed to PROJ for
+/// transforms) with a best-effort `authority:code` extracted from its trailing
+/// `AUTHORITY[...]` / `ID[...]` tag. The `OGC:CRS84` / `EPSG:4326` lon/lat
+/// special case is recognized for the Geography type.
 pub fn deserialize_crs(crs_str: &str) -> Result<Crs> {
     if crs_str.is_empty() {
         return Ok(None);
@@ -54,11 +58,14 @@ pub fn deserialize_crs(crs_str: &str) -> Result<Crs> {
 
     // Handle "0", "{AUTH}:{CODE}" authority codes (including the OGC:CRS84 /
     // EPSG:4326 lon/lat aliases, which are kept verbatim rather than folded),
-    // and PROJJSON strings.
+    // a WKT1/WKT2 CRS string, then PROJJSON.
     let crs = if crs_str == "0" {
         None
     } else if AuthorityCode::is_authority_code(crs_str) {
         AuthorityCode::crs(crs_str)
+    } else if looks_like_wkt(crs_str) {
+        Some(Arc::new(WktCrs::new(crs_str.to_string()))
+            as Arc<dyn CoordinateReferenceSystem + Send + Sync>)
     } else {
         // Try to parse as PROJJSON string
         let projjson: ProjJSON = crs_str.parse()?;
@@ -464,35 +471,42 @@ impl CoordinateReferenceSystem for AuthorityCode {
     /// Hard-code support for several known spherical CRSes so we can support them
     /// in the geography type
     fn geographic_params(&self) -> Result<Option<GeographicCrsParams>> {
-        match self.auth_code.as_str() {
-            // Default lnglat(). Here we use S2Earth::RadiusMeters() as a constant
-            // for consistent results (if we averaged over the ensemble, distance results
-            // may be subtly different between versions as the ensemble is updated).
-            "OGC:CRS84" | "EPSG:4326" => Ok(Some(GeographicCrsParams {
-                spherical_radius_m: 6371010.0,
-            })),
-            // NAD83
-            // https://spatialreference.org/ref/epsg/4269/
-            "OGC:CRS83" | "EPSG:4269" => {
-                const A: f64 = 6378137.0;
-                const INV_F: f64 = 298.257222101;
-                const B: f64 = A * (1.0 - 1.0 / INV_F);
-                Ok(Some(GeographicCrsParams {
-                    spherical_radius_m: (2.0 * A + B) / 3.0,
-                }))
-            }
-            // NAD27
-            // Mean radius = (2a + b) / 3
-            // https://spatialreference.org/ref/epsg/4267/
-            "OGC:CRS27" | "EPSG:4267" => {
-                const A: f64 = 6378206.4;
-                const B: f64 = 6356583.8;
-                Ok(Some(GeographicCrsParams {
-                    spherical_radius_m: (2.0 * A + B) / 3.0,
-                }))
-            }
-            _ => Ok(None),
+        Ok(geographic_params_for_authority(&self.auth_code))
+    }
+}
+
+/// Spherical parameters for the handful of geographic CRSes the Geography type
+/// supports, keyed by `authority:code`. Shared by [`AuthorityCode`] and [`WktCrs`]
+/// (which resolves its authority code from the WKT first).
+fn geographic_params_for_authority(auth_code: &str) -> Option<GeographicCrsParams> {
+    match auth_code {
+        // Default lnglat(). Here we use S2Earth::RadiusMeters() as a constant
+        // for consistent results (if we averaged over the ensemble, distance results
+        // may be subtly different between versions as the ensemble is updated).
+        "OGC:CRS84" | "EPSG:4326" => Some(GeographicCrsParams {
+            spherical_radius_m: 6371010.0,
+        }),
+        // NAD83
+        // https://spatialreference.org/ref/epsg/4269/
+        "OGC:CRS83" | "EPSG:4269" => {
+            const A: f64 = 6378137.0;
+            const INV_F: f64 = 298.257222101;
+            const B: f64 = A * (1.0 - 1.0 / INV_F);
+            Some(GeographicCrsParams {
+                spherical_radius_m: (2.0 * A + B) / 3.0,
+            })
         }
+        // NAD27
+        // Mean radius = (2a + b) / 3
+        // https://spatialreference.org/ref/epsg/4267/
+        "OGC:CRS27" | "EPSG:4267" => {
+            const A: f64 = 6378206.4;
+            const B: f64 = 6356583.8;
+            Some(GeographicCrsParams {
+                spherical_radius_m: (2.0 * A + B) / 3.0,
+            })
+        }
+        _ => None,
     }
 }
 
@@ -634,6 +648,319 @@ impl CoordinateReferenceSystem for ProjJSON {
     }
 }
 
+/// WKT CRS keywords that begin a recognized WKT1/WKT2 coordinate reference
+/// system, matched case-insensitively against the trimmed input.
+const WKT_CRS_KEYWORDS: &[&str] = &[
+    "PROJCRS",
+    "PROJCS",
+    "GEOGCRS",
+    "GEOGCS",
+    "GEODCRS",
+    "GEODETICCRS",
+    "GEOCCS",
+    "BOUNDCRS",
+    "COMPOUNDCRS",
+    "COMPD_CS",
+    "VERTCRS",
+    "VERT_CS",
+    "ENGCRS",
+    "ENGINEERINGCRS",
+    "PARAMETRICCRS",
+    "TIMECRS",
+    "LOCAL_CS",
+    "FITTED_CS",
+];
+
+/// Heuristic check for a WKT1/WKT2 CRS string: the trimmed value begins with a
+/// recognized CRS keyword followed (after optional whitespace) by `[` or `(`.
+fn looks_like_wkt(s: &str) -> bool {
+    let upper = s.trim_start().to_ascii_uppercase();
+    WKT_CRS_KEYWORDS.iter().any(|kw| {
+        upper
+            .strip_prefix(kw)
+            .is_some_and(|rest| rest.trim_start().starts_with(['[', '(']))
+    })
+}
+
+/// A node in a WKT CRS string. WKT is JSON-ish: a keyword followed by a
+/// bracketed, comma-separated list of children, interspersed with quoted
+/// strings and bare tokens (numbers, axis directions, enum values) as leaves.
+enum WktNode<'a> {
+    /// A bare token — a number or unquoted keyword value, e.g. `6378137`.
+    Unquoted(&'a str),
+    /// A quoted string, e.g. `"WGS 84"`.
+    Quoted(&'a str),
+    /// `KEYWORD[child, child, ...]` (or `(...)`), e.g. `ID["EPSG",4326]`.
+    Tagged(&'a str, Vec<WktNode<'a>>),
+}
+
+impl WktNode<'_> {
+    /// The inner text of a leaf node (quoted string or bare token), or `None`
+    /// for a tagged node.
+    fn leaf_str(&self) -> Option<&str> {
+        match self {
+            WktNode::Quoted(s) | WktNode::Unquoted(s) => Some(s),
+            WktNode::Tagged(..) => None,
+        }
+    }
+}
+
+/// Maximum WKT nesting depth the parser will descend into. Real CRS definitions
+/// nest under a dozen levels deep; the cap exists only to bound stack usage on
+/// pathological or malformed input (the WKT can arrive from untrusted file
+/// metadata), past which authority extraction gives up.
+const WKT_MAX_DEPTH: usize = 128;
+
+/// Recursive-descent parser over a WKT string. Whitespace is insignificant
+/// except inside quoted strings. Recursion is bounded by [`WKT_MAX_DEPTH`].
+struct WktParser<'a> {
+    s: &'a str,
+    pos: usize,
+}
+
+impl<'a> WktParser<'a> {
+    fn skip_ws(&mut self) {
+        self.pos += self.s[self.pos..]
+            .find(|c: char| !c.is_whitespace())
+            .unwrap_or(self.s.len() - self.pos);
+    }
+
+    fn peek(&self) -> Option<char> {
+        self.s[self.pos..].chars().next()
+    }
+
+    /// Parse a quoted string, consuming the surrounding quotes. A doubled `""`
+    /// is the WKT escape for a literal quote and does not terminate the string.
+    fn parse_quoted(&mut self) -> Result<WktNode<'a>, DataFusionError> {
+        self.pos += 1; // opening quote
+        let start = self.pos;
+        loop {
+            let rest = &self.s[self.pos..];
+            let Some(q) = rest.find('"') else {
+                return Err(DataFusionError::Execution(
+                    "WKT CRS string has an unterminated quoted string".into(),
+                ));
+            };
+            // A doubled "" is an escaped quote: skip both and keep scanning.
+            if rest[q + 1..].starts_with('"') {
+                self.pos += q + 2;
+                continue;
+            }
+            let value = &self.s[start..self.pos + q];
+            self.pos += q + 1; // closing quote
+            return Ok(WktNode::Quoted(value));
+        }
+    }
+
+    fn parse_node(&mut self, depth: usize) -> Result<WktNode<'a>, DataFusionError> {
+        if depth > WKT_MAX_DEPTH {
+            return Err(DataFusionError::Execution(format!(
+                "WKT CRS string nests beyond {WKT_MAX_DEPTH} levels"
+            )));
+        }
+        self.skip_ws();
+        match self.peek() {
+            Some('"') => return self.parse_quoted(),
+            Some(_) => {}
+            None => {
+                return Err(DataFusionError::Execution(
+                    "unexpected end of WKT CRS string".into(),
+                ))
+            }
+        }
+        // A keyword or bare token runs until a structural character.
+        let start = self.pos;
+        let word_len = self.s[self.pos..]
+            .find(|c: char| c.is_whitespace() || "[](),\"".contains(c))
+            .unwrap_or(self.s.len() - self.pos);
+        let word = self.s[start..start + word_len].trim();
+        self.pos += word_len;
+        self.skip_ws();
+
+        // A `[`/`(` makes this a tagged node; otherwise it is a bare token.
+        match self.peek() {
+            Some(open @ ('[' | '(')) => {
+                let close = if open == '[' { ']' } else { ')' };
+                self.pos += 1;
+                let mut children = Vec::new();
+                loop {
+                    self.skip_ws();
+                    match self.peek() {
+                        Some(c) if c == close => {
+                            self.pos += 1;
+                            break;
+                        }
+                        Some(',') => {
+                            self.pos += 1;
+                        }
+                        Some(_) => children.push(self.parse_node(depth + 1)?),
+                        None => {
+                            return Err(DataFusionError::Execution(
+                                "WKT CRS string has an unterminated bracket".into(),
+                            ))
+                        }
+                    }
+                }
+                Ok(WktNode::Tagged(word, children))
+            }
+            _ if word.is_empty() => Err(DataFusionError::Execution(
+                "WKT CRS string has an empty token".into(),
+            )),
+            _ => Ok(WktNode::Unquoted(word)),
+        }
+    }
+}
+
+/// Best-effort `authority:code` from a WKT CRS string, read from the **CRS-level**
+/// `AUTHORITY["auth","code"]` (WKT1) or `ID["auth",code]` (WKT2) tag — i.e. a
+/// direct child of the root node. Authorities on nested datums, ellipsoids,
+/// projection methods, parameters, or units are deliberately ignored: a custom
+/// CRS with no top-level authority must not be misidentified by an inner code
+/// (e.g. a Lambert Conformal whose only `ID`s are the `EPSG:9001` metre unit and
+/// projection operation codes). `None` when the root carries no authority tag.
+fn extract_authority_from_wkt(wkt: &str) -> Option<String> {
+    let mut parser = WktParser { s: wkt, pos: 0 };
+    // A malformed or too-deeply-nested WKT simply yields no authority — the
+    // definition is still carried verbatim, it just has no SRID.
+    let Ok(WktNode::Tagged(_, children)) = parser.parse_node(0) else {
+        return None;
+    };
+    for child in &children {
+        let WktNode::Tagged(keyword, args) = child else {
+            continue;
+        };
+        if !keyword.eq_ignore_ascii_case("AUTHORITY") && !keyword.eq_ignore_ascii_case("ID") {
+            continue;
+        }
+        let authority = args.first()?.leaf_str()?.trim();
+        let code = args.get(1)?.leaf_str()?.trim();
+        if authority.is_empty() || code.is_empty() {
+            return None;
+        }
+        return Some(format!("{}:{}", authority.to_uppercase(), code));
+    }
+    None
+}
+
+/// True if `keyword` is a geographic/geodetic CRS root — one with a meaningful
+/// ellipsoid for spherical geography. Projected, engineering, vertical, etc.
+/// roots are excluded so they never resolve geographic parameters.
+fn is_geographic_crs_keyword(keyword: &str) -> bool {
+    ["GEOGCS", "GEOGCRS", "GEODCRS", "GEODETICCRS"]
+        .iter()
+        .any(|k| keyword.eq_ignore_ascii_case(k))
+}
+
+/// Depth-first search for the first `ELLIPSOID` (WKT2) or `SPHEROID` (WKT1) node.
+fn find_ellipsoid<'a, 'n>(node: &'n WktNode<'a>) -> Option<&'n WktNode<'a>> {
+    let WktNode::Tagged(keyword, children) = node else {
+        return None;
+    };
+    if keyword.eq_ignore_ascii_case("ELLIPSOID") || keyword.eq_ignore_ascii_case("SPHEROID") {
+        return Some(node);
+    }
+    children.iter().find_map(find_ellipsoid)
+}
+
+/// Best-effort geographic parameters parsed straight from a WKT definition, for
+/// geographic CRSes whose authority code isn't in the curated lookup (including
+/// authority-less ones). Reads the ellipsoid's semi-major axis and inverse
+/// flattening and reduces them to a mean spherical radius the same way
+/// [`geographic_params_for_authority`] does; a sphere (inverse flattening 0)
+/// uses its radius directly. Returns `None` for non-geographic CRSes (e.g. a
+/// projected CRS, even though it nests a geographic base) or WKT without a
+/// parseable ellipsoid.
+fn geographic_params_from_wkt(wkt: &str) -> Option<GeographicCrsParams> {
+    let root = WktParser { s: wkt, pos: 0 }.parse_node(0).ok()?;
+    let WktNode::Tagged(keyword, _) = &root else {
+        return None;
+    };
+    if !is_geographic_crs_keyword(keyword) {
+        return None;
+    }
+
+    let WktNode::Tagged(_, ellipsoid_args) = find_ellipsoid(&root)? else {
+        return None;
+    };
+    let semi_major: f64 = ellipsoid_args.get(1)?.leaf_str()?.trim().parse().ok()?;
+    let inverse_flattening: f64 = ellipsoid_args.get(2)?.leaf_str()?.trim().parse().ok()?;
+    if !semi_major.is_finite() || semi_major <= 0.0 {
+        return None;
+    }
+
+    let spherical_radius_m = if inverse_flattening == 0.0 {
+        semi_major
+    } else {
+        let semi_minor = semi_major * (1.0 - 1.0 / inverse_flattening);
+        (2.0 * semi_major + semi_minor) / 3.0
+    };
+    Some(GeographicCrsParams { spherical_radius_m })
+}
+
+/// A CRS given as a WKT1/WKT2 string.
+///
+/// The WKT is preserved verbatim (`to_crs_string`) so it can be handed to PROJ
+/// for transforms, while an `authority:code` is extracted once at construction
+/// for equality and SRID. Geographic parameters resolve from that authority
+/// when it's known, otherwise straight from the WKT's own ellipsoid.
+#[derive(Debug)]
+struct WktCrs {
+    wkt: String,
+    authority_code: Option<String>,
+}
+
+impl WktCrs {
+    fn new(wkt: String) -> Self {
+        let authority_code = extract_authority_from_wkt(&wkt);
+        Self {
+            wkt,
+            authority_code,
+        }
+    }
+}
+
+impl CoordinateReferenceSystem for WktCrs {
+    /// Emit the WKT verbatim (as a JSON string), preserving the definition the
+    /// same way [`ProjJSON::to_json`] does — a WKT carrying an embedded
+    /// authority is **not** collapsed to that code, so the full definition
+    /// survives a metadata round-trip and `ST_Crs` reads it back unchanged.
+    /// (The GeoParquet writer resolves it to PROJJSON, or to the lon/lat
+    /// default, via the CRS provider, like any other definition.)
+    fn to_json(&self) -> String {
+        Value::String(self.wkt.clone()).to_string()
+    }
+
+    fn to_authority_code(&self) -> Result<Option<String>> {
+        Ok(self.authority_code.clone())
+    }
+
+    fn crs_equals(&self, other: &dyn CoordinateReferenceSystem) -> bool {
+        if let (Ok(Some(a)), Ok(Some(b))) = (self.to_authority_code(), other.to_authority_code()) {
+            return authority_codes_equal(&a, &b);
+        }
+        // No authority on one side: fall back to comparing raw definitions.
+        self.to_crs_string() == other.to_crs_string()
+    }
+
+    // `srid()` uses the trait default (derived from `to_authority_code`).
+
+    /// The verbatim WKT — the form PROJ/GDAL consume directly.
+    fn to_crs_string(&self) -> String {
+        self.wkt.clone()
+    }
+
+    fn geographic_params(&self) -> Result<Option<GeographicCrsParams>> {
+        // Prefer the curated authority lookup (stable, version-independent
+        // radii); otherwise derive the ellipsoid straight from the WKT so
+        // authority-less geographic CRSes still work.
+        Ok(self
+            .authority_code
+            .as_deref()
+            .and_then(geographic_params_for_authority)
+            .or_else(|| geographic_params_from_wkt(&self.wkt)))
+    }
+}
+
 pub const OGC_CRS84_PROJJSON: &str = r#"{"$schema":"https://proj.org/schemas/v0.7/projjson.schema.json","type":"GeographicCRS","name":"WGS 84 (CRS84)","datum_ensemble":{"name":"World Geodetic System 1984 ensemble","members":[{"name":"World Geodetic System 1984 (Transit)","id":{"authority":"EPSG","code":1166}},{"name":"World Geodetic System 1984 (G730)","id":{"authority":"EPSG","code":1152}},{"name":"World Geodetic System 1984 (G873)","id":{"authority":"EPSG","code":1153}},{"name":"World  [...]
 
 #[cfg(test)]
@@ -991,3 +1318,230 @@ mod test {
         assert_eq!(crs3857.to_json(), r#""EPSG:3857""#);
     }
 }
+
+#[cfg(test)]
+mod wkt_tests {
+    use super::*;
+
+    // CRS-level AUTHORITY is `EPSG:3857`; inner datum/ellipsoid tags
+    // (7030/6326/4326) must not win.
+    const WKT1_PSEUDO_MERCATOR: &str = r#"PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"],AUTHORITY["EPSG","3857"]]"#;
+    // WKT2 with a bare (unquoted) CRS-level code; the nested ELLIPSOID must not
+    // be mistaken for the CRS identifier.
+    const WKT2_WGS84: &str = r#"GEOGCRS["WGS 84",DATUM["World Geodetic System 1984",ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]]],CS[ellipsoidal,2],ID["EPSG",4326]]"#;
+    const WKT_NO_AUTHORITY: &str = r#"PROJCS["Custom",GEOGCS["Custom datum",DATUM["D",SPHEROID["S",6378137,298.257223563]]],PROJECTION["Mercator_1SP"]]"#;
+
+    // A custom projected CRS with no CRS-level authority whose inner GEOGCS *does*
+    // carry one (EPSG:4326). The projected CRS is not EPSG:4326 — the base
+    // geographic CRS is — so no authority should be extracted.
+    const WKT1_CUSTOM_LCC_INNER_AUTHORITY: &str = r#"PROJCS["Custom LCC",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],AUTHORITY["EPSG","4326"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",33],PARAMETER["standard_parallel_2",45],UNIT["metre",1]]"#;
+
+    // A native HRRR Lambert Conformal CRS that cannot be described by a single
+    // EPSG code. Its only `ID`s are nested unit/operation codes — `EPSG:9001`
+    // (metre), `EPSG:8901` (Greenwich), the `EPSG:9801` projection method, and
+    // its `EPSG:8801..8807` parameters — and the last of them (`EPSG:9001`) is
+    // exactly what a naive "last tag" scan would wrongly return as the CRS.
+    const WKT2_HRRR_LCC: &str = r#"PROJCRS["HRRR Lambert Conformal",BASEGEOGCRS["unknown",DATUM["unknown",ELLIPSOID["unknown",6371229,0,LENGTHUNIT["metre",1,ID["EPSG",9001]]]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8901]]],CONVERSION["unknown",METHOD["Lambert Conic Conformal (1SP)",ID["EPSG",9801]],PARAMETER["Latitude of natural origin",38.5,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],PARAMETER["Longitude of natural origin",-97.5,ANGLEUNIT["degr [...]
+
+    #[test]
+    fn detects_wkt_strings() {
+        assert!(looks_like_wkt(WKT1_PSEUDO_MERCATOR));
+        assert!(looks_like_wkt(WKT2_WGS84));
+        assert!(looks_like_wkt("  geogcs[\"x\"]")); // leading ws + lowercase
+        assert!(!looks_like_wkt("EPSG:4326"));
+        assert!(!looks_like_wkt(r#"{"type":"GeographicCRS"}"#));
+        assert!(!looks_like_wkt(r#"PROJECTILE["not a crs"]"#));
+    }
+
+    #[test]
+    fn extracts_crs_level_authority_not_inner_tags() {
+        assert_eq!(
+            extract_authority_from_wkt(WKT1_PSEUDO_MERCATOR).as_deref(),
+            Some("EPSG:3857")
+        );
+    }
+
+    #[test]
+    fn extracts_wkt2_bare_code_and_ignores_ellipsoid() {
+        assert_eq!(
+            extract_authority_from_wkt(WKT2_WGS84).as_deref(),
+            Some("EPSG:4326")
+        );
+    }
+
+    #[test]
+    fn wkt_without_authority_has_no_code() {
+        assert_eq!(extract_authority_from_wkt(WKT_NO_AUTHORITY), None);
+    }
+
+    #[test]
+    fn inner_authority_does_not_win_without_root_authority() {
+        // The inner GEOGCS EPSG:4326 belongs to the base CRS, not the projected
+        // CRS, so the custom LCC has no authority of its own.
+        assert_eq!(
+            extract_authority_from_wkt(WKT1_CUSTOM_LCC_INNER_AUTHORITY),
+            None
+        );
+        // The HRRR LCC's only codes are nested unit/operation IDs; none is the
+        // CRS, so a naive scan returning EPSG:9001 (metre) would be wrong.
+        assert_eq!(extract_authority_from_wkt(WKT2_HRRR_LCC), None);
+    }
+
+    #[test]
+    fn quoted_name_with_structural_chars_is_not_parsed_as_structure() {
+        // The name carries `[`, `]`, `,`, a doubled-quote escape, and the literal
+        // text `ID` — none of which is structure, since it is all inside a quoted
+        // string. The real CRS-level authority must still win.
+        let wkt = r#"PROJCS["Bob""s [ID] zone, N",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],UNIT["metre",1],AUTHORITY["EPSG","32633"]]"#;
+        assert_eq!(
+            extract_authority_from_wkt(wkt).as_deref(),
+            Some("EPSG:32633")
+        );
+    }
+
+    #[test]
+    fn unit_nested_authority_does_not_win() {
+        // WKT1 analog of the HRRR trap: the only authority is the metre UNIT's
+        // EPSG:9001, nested below the root, so no CRS authority is extracted.
+        let wkt = r#"PROJCS["Custom TM",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]]],PROJECTION["Transverse_Mercator"],UNIT["metre",1,AUTHORITY["EPSG","9001"]]]"#;
+        assert_eq!(extract_authority_from_wkt(wkt), None);
+    }
+
+    #[test]
+    fn parser_handles_whitespace_and_lowercase_keywords() {
+        // Pretty-printed across lines, indented, and lowercase keywords.
+        let wkt = "geogcrs[\"WGS 84\",\n    datum[\"World Geodetic System 1984\",\n        ellipsoid[\"WGS 84\",6378137,298.257223563]],\n    cs[ellipsoidal,2],\n    id[\"epsg\",4326]]";
+        assert_eq!(
+            extract_authority_from_wkt(wkt).as_deref(),
+            Some("EPSG:4326")
+        );
+    }
+
+    #[test]
+    fn malformed_wkt_returns_none_without_panicking() {
+        for input in [
+            "",
+            "EPSG:4326",
+            "PROJCS",        // keyword, no bracket
+            "PROJCS[",       // unterminated node
+            "GEOGCRS[\"x\"", // unterminated, dangling child
+            "GEOGCRS[ID[",   // unterminated nested node
+            "PROJCS[\"unterminated name",
+        ] {
+            assert_eq!(extract_authority_from_wkt(input), None, "input: {input:?}");
+        }
+    }
+
+    #[test]
+    fn deeply_nested_wkt_is_bounded() {
+        // Pathological nesting must not overflow the stack; extraction bails out
+        // (and logs a warning) well before that, returning None.
+        let deep = format!("GEOGCRS{}", "[A".repeat(WKT_MAX_DEPTH + 500));
+        assert_eq!(extract_authority_from_wkt(&deep), None);
+    }
+
+    #[test]
+    fn wkt2_id_with_trailing_version_is_extracted() {
+        // A third `ID` argument (version) must not prevent extraction.
+        let wkt = r#"GEOGCRS["WGS 84",DATUM["World Geodetic System 1984",ELLIPSOID["WGS 84",6378137,298.257223563]],CS[ellipsoidal,2],ID["EPSG",4326,"2024"]]"#;
+        assert_eq!(
+            extract_authority_from_wkt(wkt).as_deref(),
+            Some("EPSG:4326")
+        );
+    }
+
+    #[test]
+    fn custom_lcc_crs_has_no_srid_and_carries_definition_through() {
+        // A custom Lambert Conformal CRS that cannot be represented as an EPSG
+        // code must not be misidentified: no authority, no SRID, no spurious
+        // equality with the nested unit code, and the WKT survives verbatim.
+        let crs = deserialize_crs(WKT2_HRRR_LCC).unwrap().unwrap();
+        assert_eq!(crs.to_authority_code().unwrap(), None);
+        assert_eq!(crs.srid().unwrap(), None);
+        assert_eq!(crs.to_crs_string(), WKT2_HRRR_LCC);
+        let metre_unit = deserialize_crs("EPSG:9001").unwrap().unwrap();
+        assert!(!crs.crs_equals(metre_unit.as_ref()));
+    }
+
+    #[test]
+    fn deserialize_routes_wkt_to_authority_and_preserves_definition() {
+        let crs = deserialize_crs(WKT1_PSEUDO_MERCATOR).unwrap().unwrap();
+        assert_eq!(
+            crs.to_authority_code().unwrap().as_deref(),
+            Some("EPSG:3857")
+        );
+        assert_eq!(crs.srid().unwrap(), Some(3857));
+        // PROJ/GDAL get the verbatim WKT, not the authority code.
+        assert_eq!(crs.to_crs_string(), WKT1_PSEUDO_MERCATOR);
+    }
+
+    #[test]
+    fn wkt_without_authority_carries_definition_through() {
+        let crs = deserialize_crs(WKT_NO_AUTHORITY).unwrap().unwrap();
+        assert_eq!(crs.to_authority_code().unwrap(), None);
+        assert_eq!(crs.srid().unwrap(), None);
+        assert_eq!(crs.to_crs_string(), WKT_NO_AUTHORITY);
+    }
+
+    #[test]
+    fn wkt_equals_matching_authority_code() {
+        let wkt_crs = deserialize_crs(WKT1_PSEUDO_MERCATOR).unwrap().unwrap();
+        let auth_crs = deserialize_crs("EPSG:3857").unwrap().unwrap();
+        assert!(wkt_crs.crs_equals(auth_crs.as_ref()));
+        assert!(auth_crs.crs_equals(wkt_crs.as_ref()));
+    }
+
+    #[test]
+    fn wkt_geographic_params_resolve_via_authority() {
+        let geographic = deserialize_crs(WKT2_WGS84).unwrap().unwrap();
+        assert!(geographic.geographic_params().unwrap().is_some()); // EPSG:4326
+        let projected = deserialize_crs(WKT1_PSEUDO_MERCATOR).unwrap().unwrap();
+        assert!(projected.geographic_params().unwrap().is_none()); // EPSG:3857
+    }
+
+    #[test]
+    fn wkt_geographic_params_parsed_from_ellipsoid_when_authority_unknown() {
+        // An authority-less geographic WKT: params are read from its ellipsoid
+        // (GRS 1980, a=6378137, 1/f=298.257222101) and reduced to a mean radius.
+        let wkt = r#"GEOGCRS["unknown",DATUM["unknown",ELLIPSOID["GRS 1980",6378137,298.257222101,LENGTHUNIT["metre",1]]],CS[ellipsoidal,2],AXIS["lat",north],AXIS["lon",east]]"#;
+        let crs = deserialize_crs(wkt).unwrap().unwrap();
+        let params = crs.geographic_params().unwrap().expect("geographic params");
+        let a = 6378137.0_f64;
+        let b = a * (1.0 - 1.0 / 298.257222101);
+        assert!((params.spherical_radius() - (2.0 * a + b) / 3.0).abs() < 1e-6);
+    }
+
+    #[test]
+    fn wkt_sphere_radius_parsed_from_ellipsoid() {
+        // A spherical ellipsoid (inverse flattening 0) uses its radius directly.
+        let wkt = r#"GEOGCRS["sphere",DATUM["d",ELLIPSOID["sphere",6371229,0,LENGTHUNIT["metre",1]]],CS[ellipsoidal,2]]"#;
+        let crs = deserialize_crs(wkt).unwrap().unwrap();
+        let params = crs.geographic_params().unwrap().expect("geographic params");
+        assert_eq!(params.spherical_radius(), 6371229.0);
+    }
+
+    #[test]
+    fn projected_wkt_has_no_geographic_params() {
+        // A projected CRS is never a geography, even though it nests a GEOGCS
+        // with a SPHEROID — the parse fallback must reject it.
+        let crs = deserialize_crs(WKT_NO_AUTHORITY).unwrap().unwrap();
+        assert!(crs.geographic_params().unwrap().is_none());
+    }
+
+    #[test]
+    fn wkt_to_json_preserves_definition() {
+        // to_json emits the WKT verbatim (as a quoted JSON string), like
+        // ProjJSON — a WKT carrying an embedded authority is NOT collapsed to
+        // that code, so the full definition survives a metadata round-trip.
+        let crs = deserialize_crs(WKT1_PSEUDO_MERCATOR).unwrap().unwrap();
+        assert_eq!(
+            crs.to_json(),
+            Value::String(WKT1_PSEUDO_MERCATOR.to_string()).to_string()
+        );
+        let bare = deserialize_crs(WKT_NO_AUTHORITY).unwrap().unwrap();
+        assert_eq!(
+            bare.to_json(),
+            Value::String(WKT_NO_AUTHORITY.to_string()).to_string()
+        );
+    }
+}