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/05/22 03:40:37 UTC

(sedona-db) branch main updated: feat(rust/sedona-functions,c/sedona-proj): Support geography in CRS/SRID functions (#848)

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 e07a2435d feat(rust/sedona-functions,c/sedona-proj): Support geography in CRS/SRID functions (#848)
e07a2435d is described below

commit e07a2435dea2828046831834339b465c33a5b1a8
Author: Dewey Dunnington <de...@dunnington.ca>
AuthorDate: Thu May 21 22:40:32 2026 -0500

    feat(rust/sedona-functions,c/sedona-proj): Support geography in CRS/SRID functions (#848)
---
 c/sedona-proj/src/st_transform.rs           | 293 +++++++++++++++++++++++-----
 rust/sedona-functions/src/st_geomfromwkb.rs |  29 ++-
 rust/sedona-functions/src/st_geomfromwkt.rs |  21 +-
 rust/sedona-functions/src/st_point.rs       |  16 +-
 rust/sedona-functions/src/st_setsrid.rs     | 198 ++++++++++++++++++-
 rust/sedona-geometry/src/transform.rs       |   7 +
 rust/sedona-schema/src/crs.rs               | 270 +++++++++++++++++++++++++
 rust/sedona-schema/src/datatypes.rs         |  19 +-
 8 files changed, 784 insertions(+), 69 deletions(-)

diff --git a/c/sedona-proj/src/st_transform.rs b/c/sedona-proj/src/st_transform.rs
index 9128485c4..f6fa9b096 100644
--- a/c/sedona-proj/src/st_transform.rs
+++ b/c/sedona-proj/src/st_transform.rs
@@ -23,13 +23,14 @@ use datafusion_common::config::ConfigOptions;
 use datafusion_common::{exec_err, DataFusionError, Result, ScalarValue};
 use datafusion_expr::ColumnarValue;
 use sedona_common::sedona_internal_err;
-use sedona_expr::item_crs::make_item_crs;
+use sedona_expr::item_crs::{make_item_crs, parse_item_crs_arg_type};
 use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel};
 use sedona_functions::executor::WkbExecutor;
+use sedona_functions::st_setsrid::{validate_crs_array_for_type, validate_crs_for_type};
 use sedona_geometry::transform::{transform, CrsEngine, CrsTransform};
 use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES;
 use sedona_schema::crs::{deserialize_crs, Crs};
-use sedona_schema::datatypes::{Edges, SedonaType, WKB_GEOMETRY, WKB_GEOMETRY_ITEM_CRS};
+use sedona_schema::datatypes::{Edges, SedonaType};
 use sedona_schema::matchers::ArgMatcher;
 use std::io::Write;
 use std::iter::zip;
@@ -59,35 +60,52 @@ impl SedonaScalarKernel for STTransform {
         if inputs.len() == 2 {
             match (inputs[0], inputs[1]) {
                 // ScalarCrs output always returns a Wkb output type with concrete Crs
-                (ArgInput::Geo(_), ArgInput::ScalarCrs(scalar_value))
-                | (ArgInput::ItemCrs, ArgInput::ScalarCrs(scalar_value)) => {
-                    Ok(Some(output_type_from_scalar_crs_value(scalar_value)?))
-                }
+                (ArgInput::Geo(edges, _), ArgInput::ScalarCrs(scalar_value))
+                | (ArgInput::ItemCrs(edges), ArgInput::ScalarCrs(scalar_value)) => Ok(Some(
+                    output_type_from_scalar_crs_value(edges, scalar_value)?,
+                )),
 
                 // Geo or ItemCrs with ArrayCrs output always return ItemCrs output
-                (ArgInput::Geo(_), ArgInput::ArrayCrs)
-                | (ArgInput::ItemCrs, ArgInput::ArrayCrs) => {
-                    Ok(Some(WKB_GEOMETRY_ITEM_CRS.clone()))
-                }
+                (ArgInput::Geo(edges, _), ArgInput::ArrayCrs)
+                | (ArgInput::ItemCrs(edges), ArgInput::ArrayCrs) => Ok(Some(
+                    SedonaType::new_item_crs(&SedonaType::Wkb(edges, None))?,
+                )),
                 _ => Ok(None),
             }
         } else if inputs.len() == 3 {
             match (inputs[0], inputs[1], inputs[2]) {
                 // ScalarCrs output always returns a Wkb output type with concrete Crs
-                (ArgInput::Geo(_), ArgInput::ScalarCrs(_), ArgInput::ScalarCrs(scalar_value))
-                | (ArgInput::Geo(_), ArgInput::ArrayCrs, ArgInput::ScalarCrs(scalar_value))
-                | (ArgInput::ItemCrs, ArgInput::ScalarCrs(_), ArgInput::ScalarCrs(scalar_value))
-                | (ArgInput::ItemCrs, ArgInput::ArrayCrs, ArgInput::ScalarCrs(scalar_value)) => {
-                    Ok(Some(output_type_from_scalar_crs_value(scalar_value)?))
-                }
+                (
+                    ArgInput::Geo(edges, _),
+                    ArgInput::ScalarCrs(_),
+                    ArgInput::ScalarCrs(scalar_value),
+                )
+                | (
+                    ArgInput::Geo(edges, _),
+                    ArgInput::ArrayCrs,
+                    ArgInput::ScalarCrs(scalar_value),
+                )
+                | (
+                    ArgInput::ItemCrs(edges),
+                    ArgInput::ScalarCrs(_),
+                    ArgInput::ScalarCrs(scalar_value),
+                )
+                | (
+                    ArgInput::ItemCrs(edges),
+                    ArgInput::ArrayCrs,
+                    ArgInput::ScalarCrs(scalar_value),
+                ) => Ok(Some(output_type_from_scalar_crs_value(
+                    edges,
+                    scalar_value,
+                )?)),
 
                 // Geo or ItemCrs with ArrayCrs output always return ItemCrs output
-                (ArgInput::Geo(_), ArgInput::ScalarCrs(_), ArgInput::ArrayCrs)
-                | (ArgInput::Geo(_), ArgInput::ArrayCrs, ArgInput::ArrayCrs)
-                | (ArgInput::ItemCrs, ArgInput::ScalarCrs(_), ArgInput::ArrayCrs)
-                | (ArgInput::ItemCrs, ArgInput::ArrayCrs, ArgInput::ArrayCrs) => {
-                    Ok(Some(WKB_GEOMETRY_ITEM_CRS.clone()))
-                }
+                (ArgInput::Geo(edges, _), ArgInput::ScalarCrs(_), ArgInput::ArrayCrs)
+                | (ArgInput::Geo(edges, _), ArgInput::ArrayCrs, ArgInput::ArrayCrs)
+                | (ArgInput::ItemCrs(edges), ArgInput::ScalarCrs(_), ArgInput::ArrayCrs)
+                | (ArgInput::ItemCrs(edges), ArgInput::ArrayCrs, ArgInput::ArrayCrs) => Ok(Some(
+                    SedonaType::new_item_crs(&SedonaType::Wkb(edges, None))?,
+                )),
                 _ => Ok(None),
             }
         } else {
@@ -99,7 +117,7 @@ impl SedonaScalarKernel for STTransform {
         &self,
         arg_types: &[SedonaType],
         args: &[ColumnarValue],
-        _return_type: &SedonaType,
+        return_type: &SedonaType,
         _num_rows: usize,
         _config_options: Option<&ConfigOptions>,
     ) -> Result<ColumnarValue> {
@@ -113,6 +131,8 @@ impl SedonaScalarKernel for STTransform {
             WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
         );
 
+        let (return_item_type, _) = parse_item_crs_arg_type(return_type)?;
+
         // Optimize the easy case, where we have exactly one transformation and there are no
         // null or missing CRSes to contend with.
         let from_index = inputs.len() - 2;
@@ -123,6 +143,11 @@ impl SedonaScalarKernel for STTransform {
             let maybe_from_crs = deserialize_crs(&from_constant)?;
             let maybe_to_crs = deserialize_crs(&to_constant)?;
             if let (Some(from_crs), Some(to_crs)) = (maybe_from_crs, maybe_to_crs) {
+                // Validate the target CRS is valid for the output type (e.g., geography
+                // requires a geographic CRS)
+                let to_crs_opt: Crs = Some(to_crs.clone());
+                validate_crs_for_type(&to_crs_opt, &return_item_type)?;
+
                 with_global_proj_engine(|engine| {
                     let crs_transform = engine
                         .get_transform_crs_to_crs(
@@ -148,9 +173,15 @@ impl SedonaScalarKernel for STTransform {
             }
         }
 
-        // Iterate over pairs of CRS strings
+        // Get the CRS input arrays
         let from_crs_array = from.crs_array(&args[from_index], executor.num_iterations())?;
         let to_crs_array = to.crs_array(&args[to_index], executor.num_iterations())?;
+
+        // Validate the to_crs_array if needed to ensure the output CRS is valid
+        // for geography types
+        validate_crs_array_for_type(&to_crs_array, &return_item_type)?;
+
+        // Iterate over pairs of CRS strings
         let from_crs_string_view_array = as_string_view_array(&from_crs_array)?;
         let to_crs_string_view_array = as_string_view_array(&to_crs_array)?;
         let mut crs_to_crs_iter = zip(from_crs_string_view_array, to_crs_string_view_array);
@@ -213,7 +244,7 @@ impl SedonaScalarKernel for STTransform {
         let output_geometry = executor.finish(Arc::new(builder.finish()))?;
         if let Some(mut crs_output) = maybe_crs_output {
             let output_crs = executor.finish(Arc::new(crs_output.finish()))?;
-            make_item_crs(&WKB_GEOMETRY, output_geometry, &output_crs, None)
+            make_item_crs(&return_item_type, output_geometry, &output_crs, None)
         } else {
             Ok(output_geometry)
         }
@@ -232,11 +263,14 @@ impl SedonaScalarKernel for STTransform {
     }
 }
 
-fn output_type_from_scalar_crs_value(scalar_arg: &ScalarValue) -> Result<SedonaType> {
+fn output_type_from_scalar_crs_value(
+    input_edges: Edges,
+    scalar_arg: &ScalarValue,
+) -> Result<SedonaType> {
     if let Some(crs_str) = parse_crs_from_scalar_crs_value(scalar_arg)? {
-        Ok(SedonaType::Wkb(Edges::Planar, deserialize_crs(&crs_str)?))
+        Ok(SedonaType::Wkb(input_edges, deserialize_crs(&crs_str)?))
     } else {
-        Ok(WKB_GEOMETRY)
+        Ok(SedonaType::Wkb(input_edges, None))
     }
 }
 
@@ -271,13 +305,12 @@ fn invoke_scalar(wkb: &Wkb, trans: &dyn CrsTransform, builder: &mut impl Write)
 /// Helper to label arguments because we have a lot argument types that are valid
 #[derive(Debug, Clone, Copy)]
 enum ArgInput<'a> {
-    /// Geometry input. This currently only matches geometry and not geography
-    /// because CRS support for geography is less clear at the moment. Must be
+    /// Geometry or geography input. Must be
     /// the first argument (and not supported for other arguments).
-    Geo(&'a Crs),
+    Geo(Edges, &'a Crs),
     /// Item-level CRS input. Must be the first argument if present (not supported
     /// for other arguments).
-    ItemCrs,
+    ItemCrs(Edges),
     /// Scalar CRS input. Supported for second and third arguments. When present
     /// as the last argument (to), this forces type-level CRS output.
     ScalarCrs(&'a ScalarValue),
@@ -290,9 +323,15 @@ enum ArgInput<'a> {
 
 impl<'a> ArgInput<'a> {
     fn from_return_type_arg(arg_type: &'a SedonaType, scalar_arg: Option<&'a ScalarValue>) -> Self {
-        if ArgMatcher::is_item_crs().match_type(arg_type) {
-            Self::ItemCrs
-        } else if ArgMatcher::is_numeric().match_type(arg_type)
+        if let Ok((SedonaType::Wkb(edges, _) | SedonaType::WkbView(edges, _), maybe_crs_type)) =
+            parse_item_crs_arg_type(arg_type)
+        {
+            if maybe_crs_type.is_some() {
+                return Self::ItemCrs(edges);
+            }
+        }
+
+        if ArgMatcher::is_numeric().match_type(arg_type)
             || ArgMatcher::is_string().match_type(arg_type)
         {
             if let Some(scalar_crs) = scalar_arg {
@@ -302,8 +341,8 @@ impl<'a> ArgInput<'a> {
             }
         } else {
             match arg_type {
-                SedonaType::Wkb(Edges::Planar, crs) | SedonaType::WkbView(Edges::Planar, crs) => {
-                    Self::Geo(crs)
+                SedonaType::Wkb(edges, crs) | SedonaType::WkbView(edges, crs) => {
+                    Self::Geo(*edges, crs)
                 }
                 _ => Self::Unsupported,
             }
@@ -311,9 +350,15 @@ impl<'a> ArgInput<'a> {
     }
 
     fn from_arg(arg_type: &'a SedonaType, arg: &'a ColumnarValue) -> Self {
-        if ArgMatcher::is_item_crs().match_type(arg_type) {
-            Self::ItemCrs
-        } else if ArgMatcher::is_numeric().match_type(arg_type)
+        if let Ok((SedonaType::Wkb(edges, _) | SedonaType::WkbView(edges, _), maybe_crs_type)) =
+            parse_item_crs_arg_type(arg_type)
+        {
+            if maybe_crs_type.is_some() {
+                return Self::ItemCrs(edges);
+            }
+        }
+
+        if ArgMatcher::is_numeric().match_type(arg_type)
             || ArgMatcher::is_string().match_type(arg_type)
         {
             match arg {
@@ -322,7 +367,9 @@ impl<'a> ArgInput<'a> {
             }
         } else {
             match arg_type {
-                SedonaType::Wkb(_, crs) | SedonaType::WkbView(_, crs) => Self::Geo(crs),
+                SedonaType::Wkb(edges, crs) | SedonaType::WkbView(edges, crs) => {
+                    Self::Geo(*edges, crs)
+                }
                 _ => Self::Unsupported,
             }
         }
@@ -330,7 +377,7 @@ impl<'a> ArgInput<'a> {
 
     fn crs_constant(&self) -> Result<Option<String>> {
         match self {
-            ArgInput::Geo(crs) => {
+            ArgInput::Geo(_, crs) => {
                 let crs_str = if let Some(crs) = crs {
                     crs.to_crs_string()
                 } else {
@@ -347,7 +394,7 @@ impl<'a> ArgInput<'a> {
     fn crs_array(&self, arg: &ColumnarValue, iterations: usize) -> Result<ArrayRef> {
         if let Some(crs_constant) = self.crs_constant()? {
             ScalarValue::Utf8View(Some(crs_constant)).to_array_of_size(iterations)
-        } else if matches!(self, Self::ItemCrs) {
+        } else if matches!(self, Self::ItemCrs(_)) {
             match arg {
                 ColumnarValue::Array(array) => {
                     let struct_array = as_struct_array(array)?;
@@ -375,7 +422,9 @@ mod tests {
     use sedona_expr::scalar_udf::SedonaScalarUDF;
     use sedona_schema::crs::lnglat;
     use sedona_schema::crs::Crs;
-    use sedona_schema::datatypes::WKB_GEOMETRY;
+    use sedona_schema::datatypes::{
+        WKB_GEOGRAPHY, WKB_GEOGRAPHY_ITEM_CRS, WKB_GEOMETRY, WKB_GEOMETRY_ITEM_CRS,
+    };
     use sedona_testing::compare::assert_array_equal;
     use sedona_testing::create::create_array;
     use sedona_testing::create::create_array_item_crs;
@@ -383,6 +432,7 @@ mod tests {
     use sedona_testing::testers::ScalarUdfTester;
 
     const NAD83ZONE6PROJ: &str = "EPSG:2230";
+    const NAD83_GEOGRAPHIC: &str = "EPSG:4269";
     const WGS84: &str = "EPSG:4326";
 
     #[test]
@@ -426,6 +476,42 @@ mod tests {
         assert_array_equal(&result, &expected_array);
     }
 
+    #[test]
+    fn test_invoke_with_string_geography() {
+        let udf = SedonaScalarUDF::from_impl("st_transform", st_transform_impl());
+        let geography_input = SedonaType::Wkb(Edges::Spherical, lnglat());
+        let tester = ScalarUdfTester::new(
+            udf.into(),
+            vec![geography_input.clone(), SedonaType::Arrow(DataType::Utf8)],
+        );
+
+        // Transform to geographic CRS should succeed
+        let expected_return_type = SedonaType::Wkb(Edges::Spherical, get_crs(NAD83_GEOGRAPHIC));
+        let return_type = tester
+            .return_type_with_scalar_scalar(Option::<&str>::None, Some(NAD83_GEOGRAPHIC))
+            .unwrap();
+        assert_eq!(return_type, expected_return_type);
+
+        // Return type with array to argument (returns item CRS)
+        let return_type = tester.return_type().unwrap();
+        assert_eq!(return_type, WKB_GEOGRAPHY_ITEM_CRS.clone());
+
+        // Invoke with geographic CRS should succeed
+        let wkb = create_array(&[None, Some("POINT (79.3871 43.6426)")], &geography_input);
+        let result = tester
+            .invoke_array_scalar(wkb.clone(), NAD83_GEOGRAPHIC)
+            .unwrap();
+        assert_eq!(result.len(), 2);
+
+        // Transform to projected CRS should fail for geography
+        let err = tester.invoke_array_scalar(wkb, NAD83ZONE6PROJ).unwrap_err();
+        assert!(
+            err.message().contains("Can't assign non-geographic CRS"),
+            "Expected error about non-geographic CRS, got: {}",
+            err.message()
+        );
+    }
+
     #[test]
     fn test_invoke_with_srid() {
         let udf = SedonaScalarUDF::from_impl("st_transform", st_transform_impl());
@@ -467,6 +553,36 @@ mod tests {
         assert_array_equal(&result, &expected_array);
     }
 
+    #[test]
+    fn test_invoke_with_srid_geography() {
+        let udf = SedonaScalarUDF::from_impl("st_transform", st_transform_impl());
+        let geography_input = SedonaType::Wkb(Edges::Spherical, lnglat());
+        let tester = ScalarUdfTester::new(
+            udf.into(),
+            vec![geography_input.clone(), SedonaType::Arrow(DataType::UInt32)],
+        );
+
+        // Transform to geographic CRS (4269 = NAD83) should succeed
+        let expected_return_type = SedonaType::Wkb(Edges::Spherical, get_crs(NAD83_GEOGRAPHIC));
+        let return_type = tester
+            .return_type_with_scalar_scalar(Option::<&str>::None, Some(4269))
+            .unwrap();
+        assert_eq!(return_type, expected_return_type);
+
+        // Invoke with geographic SRID should succeed
+        let wkb = create_array(&[None, Some("POINT (79.3871 43.6426)")], &geography_input);
+        let result = tester.invoke_array_scalar(wkb.clone(), 4269).unwrap();
+        assert_eq!(result.len(), 2);
+
+        // Transform to projected SRID (2230) should fail for geography
+        let err = tester.invoke_array_scalar(wkb, 2230).unwrap_err();
+        assert!(
+            err.message().contains("Can't assign non-geographic CRS"),
+            "Expected error about non-geographic CRS, got: {}",
+            err.message()
+        );
+    }
+
     #[test]
     fn test_invoke_with_item_crs() {
         let udf = SedonaScalarUDF::from_impl("st_transform", st_transform_impl());
@@ -520,6 +636,48 @@ mod tests {
         assert_array_equal(&result, &expected_array);
     }
 
+    #[test]
+    fn test_invoke_with_item_crs_geography() {
+        let udf = SedonaScalarUDF::from_impl("st_transform", st_transform_impl());
+        let geography_input = WKB_GEOGRAPHY_ITEM_CRS.clone();
+        let tester = ScalarUdfTester::new(
+            udf.into(),
+            vec![geography_input.clone(), SedonaType::Arrow(DataType::Utf8)],
+        );
+
+        // Transform to geographic CRS should succeed
+        let expected_return_type = SedonaType::Wkb(Edges::Spherical, get_crs(NAD83_GEOGRAPHIC));
+        let return_type = tester
+            .return_type_with_scalar_scalar(Option::<&str>::None, Some(NAD83_GEOGRAPHIC))
+            .unwrap();
+        assert_eq!(return_type, expected_return_type);
+
+        // Return type with array to argument (returns item CRS)
+        let return_type = tester.return_type().unwrap();
+        assert_eq!(return_type, WKB_GEOGRAPHY_ITEM_CRS.clone());
+
+        // Invoke with geographic CRS should succeed
+        let array_in = create_array_item_crs(
+            &[None, Some("POINT (79.3871 43.6426)")],
+            [None, Some("EPSG:4326")],
+            &WKB_GEOGRAPHY,
+        );
+        let result = tester
+            .invoke_array_scalar(array_in.clone(), NAD83_GEOGRAPHIC)
+            .unwrap();
+        assert_eq!(result.len(), 2);
+
+        // Transform to projected CRS should fail for geography
+        let err = tester
+            .invoke_array_scalar(array_in, NAD83ZONE6PROJ)
+            .unwrap_err();
+        assert!(
+            err.message().contains("Can't assign non-geographic CRS"),
+            "Expected error about non-geographic CRS, got: {}",
+            err.message()
+        );
+    }
+
     #[rstest]
     fn test_invoke_source_arg() {
         let udf = SedonaScalarUDF::from_impl("st_transform", st_transform_impl());
@@ -577,6 +735,53 @@ mod tests {
         assert_array_equal(&result, &expected_array);
     }
 
+    #[rstest]
+    fn test_invoke_source_arg_geography() {
+        let udf = SedonaScalarUDF::from_impl("st_transform", st_transform_impl());
+        let geography_input = WKB_GEOGRAPHY;
+        let tester = ScalarUdfTester::new(
+            udf.into(),
+            vec![
+                geography_input.clone(),
+                SedonaType::Arrow(DataType::Utf8),
+                SedonaType::Arrow(DataType::Utf8),
+            ],
+        );
+
+        // Transform to geographic CRS should succeed
+        let expected_return_type = SedonaType::Wkb(Edges::Spherical, get_crs(NAD83_GEOGRAPHIC));
+        let return_type = tester
+            .return_type_with_scalar_scalar_scalar(
+                Option::<&str>::None,
+                Option::<&str>::None,
+                Some(NAD83_GEOGRAPHIC),
+            )
+            .unwrap();
+        assert_eq!(return_type, expected_return_type);
+
+        // Return type with array to argument (returns item CRS)
+        let return_type = tester.return_type().unwrap();
+        assert_eq!(return_type, WKB_GEOGRAPHY_ITEM_CRS.clone());
+
+        // Invoke with geographic CRS should succeed
+        let array_in = create_array(&[None, Some("POINT (79.3871 43.6426)")], &geography_input);
+        let crs_from = create_array!(Utf8, [None, Some(WGS84)]) as ArrayRef;
+        let result = tester
+            .invoke_array_array_scalar(array_in.clone(), crs_from.clone(), NAD83_GEOGRAPHIC)
+            .unwrap();
+        assert_eq!(result.len(), 2);
+
+        // Transform to projected CRS should fail for geography
+        let err = tester
+            .invoke_array_array_scalar(array_in, crs_from, NAD83ZONE6PROJ)
+            .unwrap_err();
+        assert!(
+            err.message().contains("Can't assign non-geographic CRS"),
+            "Expected error about non-geographic CRS, got: {}",
+            err.message()
+        );
+    }
+
     #[test]
     fn test_invoke_null_crs_to() {
         let udf = SedonaScalarUDF::from_impl("st_transform", st_transform_impl());
diff --git a/rust/sedona-functions/src/st_geomfromwkb.rs b/rust/sedona-functions/src/st_geomfromwkb.rs
index c5a8803d0..3e85b9a6d 100644
--- a/rust/sedona-functions/src/st_geomfromwkb.rs
+++ b/rust/sedona-functions/src/st_geomfromwkb.rs
@@ -21,7 +21,9 @@ use datafusion_common::error::Result;
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
 use sedona_schema::{
-    datatypes::{SedonaType, WKB_GEOMETRY, WKB_VIEW_GEOGRAPHY, WKB_VIEW_GEOMETRY},
+    datatypes::{
+        SedonaType, WKB_GEOMETRY, WKB_VIEW_GEOGRAPHY, WKB_VIEW_GEOGRAPHY_WGS84, WKB_VIEW_GEOMETRY,
+    },
     matchers::ArgMatcher,
 };
 
@@ -61,12 +63,22 @@ pub fn st_geomfromwkbunchecked_udf() -> SedonaScalarUDF {
 ///
 /// An implementation of WKB reading using GeoRust's wkb crate.
 pub fn st_geogfromwkb_udf() -> SedonaScalarUDF {
+    // Inner kernel for SRIDified has no CRS - the SRID argument sets it
+    let inner_kernel = Arc::new(STGeomFromWKB {
+        validate: true,
+        out_type: WKB_VIEW_GEOGRAPHY,
+    });
+    let sridified_kernel = Arc::new(SRIDifiedKernel::new(inner_kernel));
+
+    // Standalone kernel returns WGS84 CRS by default
+    let standalone_kernel = Arc::new(STGeomFromWKB {
+        validate: true,
+        out_type: WKB_VIEW_GEOGRAPHY_WGS84.clone(),
+    });
+
     SedonaScalarUDF::new(
         "st_geogfromwkb",
-        vec![Arc::new(STGeomFromWKB {
-            validate: true,
-            out_type: WKB_VIEW_GEOGRAPHY,
-        })],
+        vec![sridified_kernel, standalone_kernel],
         Volatility::Immutable,
     )
 }
@@ -328,11 +340,14 @@ mod tests {
         let udf = st_geogfromwkb_udf();
         let tester = ScalarUdfTester::new(udf.into(), vec![SedonaType::Arrow(DataType::Binary)]);
 
-        assert_eq!(tester.return_type().unwrap(), WKB_VIEW_GEOGRAPHY);
+        assert_eq!(
+            tester.return_type().unwrap(),
+            WKB_VIEW_GEOGRAPHY_WGS84.clone()
+        );
 
         assert_scalar_equal(
             &tester.invoke_scalar(POINT12.to_vec()).unwrap(),
-            &create_scalar(Some("POINT (1 2)"), &WKB_VIEW_GEOGRAPHY),
+            &create_scalar(Some("POINT (1 2)"), &WKB_VIEW_GEOGRAPHY_WGS84),
         );
     }
 }
diff --git a/rust/sedona-functions/src/st_geomfromwkt.rs b/rust/sedona-functions/src/st_geomfromwkt.rs
index 79e4c084e..12b7bd17a 100644
--- a/rust/sedona-functions/src/st_geomfromwkt.rs
+++ b/rust/sedona-functions/src/st_geomfromwkt.rs
@@ -28,7 +28,7 @@ use sedona_expr::item_crs::make_item_crs;
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
 use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES;
 use sedona_schema::{
-    datatypes::{SedonaType, WKB_GEOGRAPHY, WKB_GEOMETRY},
+    datatypes::{SedonaType, WKB_GEOGRAPHY, WKB_GEOGRAPHY_WGS84, WKB_GEOMETRY},
     matchers::ArgMatcher,
 };
 use wkb::writer::{write_geometry, WriteOptions};
@@ -64,11 +64,20 @@ pub fn st_geomfromwkt_udf() -> SedonaScalarUDF {
 /// An implementation of WKT reading using GeoRust's wkt crate.
 /// See [`st_geomfromwkt_udf`] for the corresponding geometry function.
 pub fn st_geogfromwkt_udf() -> SedonaScalarUDF {
+    // Inner kernel for SRIDified has no CRS - the SRID argument sets it
+    let inner_kernel = Arc::new(STGeoFromWKT {
+        out_type: WKB_GEOGRAPHY,
+    });
+    let sridified_kernel = Arc::new(SRIDifiedKernel::new(inner_kernel));
+
+    // Standalone kernel returns WGS84 CRS by default
+    let standalone_kernel = Arc::new(STGeoFromWKT {
+        out_type: WKB_GEOGRAPHY_WGS84.clone(),
+    });
+
     let udf = SedonaScalarUDF::new(
         "st_geogfromwkt",
-        vec![Arc::new(STGeoFromWKT {
-            out_type: WKB_GEOGRAPHY,
-        })],
+        vec![sridified_kernel, standalone_kernel],
         Volatility::Immutable,
     );
     udf.with_aliases(vec!["st_geogfromtext".to_string()])
@@ -444,10 +453,10 @@ mod tests {
     fn geog() {
         let udf = st_geogfromwkt_udf();
         let tester = ScalarUdfTester::new(udf.into(), vec![SedonaType::Arrow(DataType::Utf8)]);
-        assert_eq!(tester.return_type().unwrap(), WKB_GEOGRAPHY);
+        assert_eq!(tester.return_type().unwrap(), *WKB_GEOGRAPHY_WGS84);
         assert_scalar_equal(
             &tester.invoke_scalar("POINT (1 2)").unwrap(),
-            &create_scalar(Some("POINT (1 2)"), &WKB_GEOGRAPHY),
+            &create_scalar(Some("POINT (1 2)"), &WKB_GEOGRAPHY_WGS84),
         );
     }
 
diff --git a/rust/sedona-functions/src/st_point.rs b/rust/sedona-functions/src/st_point.rs
index c9cab7ab9..1863f73f7 100644
--- a/rust/sedona-functions/src/st_point.rs
+++ b/rust/sedona-functions/src/st_point.rs
@@ -24,7 +24,7 @@ use datafusion_common::scalar::ScalarValue;
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
 use sedona_schema::{
-    datatypes::{SedonaType, WKB_GEOGRAPHY, WKB_GEOMETRY},
+    datatypes::{SedonaType, WKB_GEOGRAPHY, WKB_GEOGRAPHY_WGS84, WKB_GEOMETRY},
     matchers::ArgMatcher,
 };
 
@@ -52,14 +52,20 @@ pub fn st_point_udf() -> SedonaScalarUDF {
 /// Native implementation to create geometries from coordinates.
 /// See [`st_geogpoint_udf`] for the corresponding geography constructor.
 pub fn st_geogpoint_udf() -> SedonaScalarUDF {
-    let kernel = Arc::new(STGeoFromPoint {
+    // Inner kernel for SRIDified has no CRS - the SRID argument sets it
+    let inner_kernel = Arc::new(STGeoFromPoint {
         out_type: WKB_GEOGRAPHY,
     });
-    let sridified_kernel = Arc::new(SRIDifiedKernel::new(kernel.clone()));
+    let sridified_kernel = Arc::new(SRIDifiedKernel::new(inner_kernel));
+
+    // Standalone kernel returns WGS84 CRS by default
+    let standalone_kernel = Arc::new(STGeoFromPoint {
+        out_type: WKB_GEOGRAPHY_WGS84.clone(),
+    });
 
     SedonaScalarUDF::new(
         "st_geogpoint",
-        vec![sridified_kernel, kernel],
+        vec![sridified_kernel, standalone_kernel],
         Volatility::Immutable,
     )
 }
@@ -357,7 +363,7 @@ mod tests {
             ],
         );
 
-        tester.assert_return_type(WKB_GEOGRAPHY);
+        tester.assert_return_type(WKB_GEOGRAPHY_WGS84.clone());
         let result = tester.invoke_scalar_scalar(1.0, 2.0).unwrap();
         tester.assert_scalar_result_equals(result, "POINT (1 2)");
     }
diff --git a/rust/sedona-functions/src/st_setsrid.rs b/rust/sedona-functions/src/st_setsrid.rs
index b00fc4083..c8bb8790a 100644
--- a/rust/sedona-functions/src/st_setsrid.rs
+++ b/rust/sedona-functions/src/st_setsrid.rs
@@ -22,12 +22,12 @@ use arrow_array::{
 };
 use arrow_buffer::NullBuffer;
 use arrow_schema::DataType;
-use datafusion_common::config::ConfigOptions;
 use datafusion_common::{
     cast::{as_int64_array, as_string_view_array},
     error::Result,
-    DataFusionError, ScalarValue,
+    exec_err, DataFusionError, ScalarValue,
 };
+use datafusion_common::{config::ConfigOptions, plan_err};
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_common::sedona_internal_err;
 use sedona_expr::{
@@ -39,8 +39,8 @@ use sedona_expr::{
 };
 use sedona_geometry::transform::CrsEngine;
 use sedona_schema::{
-    crs::{deserialize_crs, CachedCrsNormalization, CachedSRIDToCrs},
-    datatypes::SedonaType,
+    crs::{deserialize_crs, CachedCrsNormalization, CachedSRIDToCrs, Crs},
+    datatypes::{Edges, SedonaType},
     matchers::ArgMatcher,
 };
 
@@ -214,6 +214,7 @@ fn invoke_set_crs(
     let item_crs_matcher = ArgMatcher::is_item_crs();
     if item_crs_matcher.match_type(return_type) {
         let normalized_crs_value = normalize_crs_array(crs_arg, maybe_engine)?;
+        validate_crs_array_for_type(&normalized_crs_value, item_type)?;
         make_item_crs(
             item_type,
             item_arg,
@@ -260,6 +261,8 @@ fn determine_return_type(
                 None => None,
             };
 
+            validate_crs_for_type(&new_crs, &item_type)?;
+
             match item_type {
                 SedonaType::Wkb(edges, _) => Ok(Some(SedonaType::Wkb(edges, new_crs))),
                 SedonaType::WkbView(edges, _) => Ok(Some(SedonaType::WkbView(edges, new_crs))),
@@ -334,6 +337,9 @@ impl SedonaScalarKernel for SRIDifiedKernel {
                 }
             };
 
+            // Check that the CRS is valid for the target type
+            validate_crs_for_type(&new_crs, &inner_result)?;
+
             match &mut inner_result {
                 SedonaType::Wkb(_, crs) => *crs = new_crs,
                 SedonaType::WkbView(_, crs) => *crs = new_crs,
@@ -404,6 +410,7 @@ impl SedonaScalarKernel for SRIDifiedKernel {
         } else {
             let (item_type, _) = parse_item_crs_arg_type(return_type)?;
             let normalized_crs_value = normalize_crs_array(&args[orig_args_len], None)?;
+            validate_crs_array_for_type(&normalized_crs_value, &item_type)?;
             make_item_crs(
                 &item_type,
                 result,
@@ -534,6 +541,67 @@ pub fn validate_crs(
     Ok(())
 }
 
+/// Given a resolved [Crs], validate that it is appropriate for a given output
+///
+/// This is primarily to enforce that the geography type may only have a
+/// geographic CRS. The unset CRS is also allowed for now but this may be
+/// updated in the future (in general a Geography type should always have a
+/// CRS).
+pub fn validate_crs_for_type(crs: &Crs, sedona_type: &SedonaType) -> Result<()> {
+    let edges = match sedona_type {
+        SedonaType::Wkb(edges, _) | SedonaType::WkbView(edges, _) => edges,
+        _ => {
+            return sedona_internal_err!(
+                "Non-geometry type in CRS--type validation: {sedona_type}"
+            );
+        }
+    };
+
+    // For geography, ensure the CRS is geographical if present
+    if !matches!(edges, Edges::Planar) {
+        if let Some(crs) = crs {
+            if crs.geographic_params()?.is_none() {
+                return plan_err!(
+                    "Can't assign non-geographic CRS {crs} to column of type {sedona_type}"
+                );
+            }
+        }
+    }
+
+    Ok(())
+}
+
+/// Given an array of CRSes (pre-normalized to stringview) check validity against type
+///
+/// This check is skipped for Geometry but occurs for Geography to ensure the
+/// target types are valid.
+pub fn validate_crs_array_for_type(crs_array: &ArrayRef, sedona_type: &SedonaType) -> Result<()> {
+    let edges = match sedona_type {
+        SedonaType::Wkb(edges, _) | SedonaType::WkbView(edges, _) => edges,
+        _ => {
+            return sedona_internal_err!(
+                "Non-geometry type in CRS--type validation: {sedona_type}"
+            );
+        }
+    };
+
+    // For geography, ensure the CRS is geographical if present
+    if !matches!(edges, Edges::Planar) {
+        let crs_array_stringview = as_string_view_array(crs_array)?;
+        for item in crs_array_stringview.iter().flatten() {
+            if let Some(crs) = deserialize_crs(item)? {
+                if crs.geographic_params()?.is_none() {
+                    return exec_err!(
+                        "Can't assign non-geographic CRS item {item} to column of type {sedona_type}"
+                    );
+                }
+            }
+        }
+    }
+
+    Ok(())
+}
+
 #[cfg(test)]
 mod test {
     use std::rc::Rc;
@@ -546,7 +614,9 @@ mod test {
     use sedona_geometry::{error::SedonaGeometryError, transform::CrsTransform};
     use sedona_schema::{
         crs::lnglat,
-        datatypes::{Edges, WKB_GEOMETRY, WKB_GEOMETRY_ITEM_CRS},
+        datatypes::{
+            Edges, WKB_GEOGRAPHY, WKB_GEOGRAPHY_ITEM_CRS, WKB_GEOMETRY, WKB_GEOMETRY_ITEM_CRS,
+        },
     };
     use sedona_testing::{
         compare::assert_value_equal,
@@ -660,6 +730,56 @@ mod test {
         assert_eq!(err.message(), "Unknown geometry error")
     }
 
+    #[test]
+    fn udf_crs_geography() {
+        let udf: ScalarUDF = st_set_crs_udf().into();
+
+        let wkb_geography_lnglat = SedonaType::Wkb(Edges::Spherical, lnglat());
+        let geog_arg = create_scalar_value(Some("POINT (0 1)"), &WKB_GEOGRAPHY);
+        let geog_lnglat = create_scalar_value(Some("POINT (0 1)"), &wkb_geography_lnglat);
+
+        let geographic_crs_scalar = ScalarValue::Utf8(Some("EPSG:4326".to_string()));
+        let projected_crs_scalar = ScalarValue::Utf8(Some("EPSG:3857".to_string()));
+        let null_crs_scalar = ScalarValue::Utf8(None);
+
+        // Call with a geographic CRS (should succeed for geography)
+        let (return_type, result) = call_udf(
+            &udf,
+            geog_arg.clone(),
+            &[WKB_GEOGRAPHY, SedonaType::Arrow(DataType::Utf8)],
+            geographic_crs_scalar.clone(),
+        )
+        .unwrap();
+        assert_eq!(return_type, wkb_geography_lnglat);
+        assert_value_equal(&result, &geog_lnglat);
+
+        // Call with a null scalar destination (result should be NULL per SQL NULL propagation)
+        let (return_type, result) = call_udf(
+            &udf,
+            geog_arg.clone(),
+            &[WKB_GEOGRAPHY, SedonaType::Arrow(DataType::Utf8)],
+            null_crs_scalar.clone(),
+        )
+        .unwrap();
+        assert_eq!(return_type, WKB_GEOGRAPHY);
+        let null_geog = create_scalar_value(None, &WKB_GEOGRAPHY);
+        assert_value_equal(&result, &null_geog);
+
+        // Call with a projected CRS (should fail for geography - only geographic CRS allowed)
+        let err = call_udf(
+            &udf,
+            geog_arg.clone(),
+            &[WKB_GEOGRAPHY, SedonaType::Arrow(DataType::Utf8)],
+            projected_crs_scalar.clone(),
+        )
+        .unwrap_err();
+        assert!(
+            err.message().contains("Can't assign non-geographic CRS"),
+            "Expected error about non-geographic CRS, got: {}",
+            err.message()
+        );
+    }
+
     #[rstest]
     fn udf_item_srid_output(
         #[values(WKB_GEOMETRY, WKB_GEOMETRY_ITEM_CRS.clone())] sedona_type: SedonaType,
@@ -764,6 +884,74 @@ mod test {
         );
     }
 
+    #[rstest]
+    fn udf_item_crs_output_geography(
+        #[values(WKB_GEOGRAPHY, WKB_GEOGRAPHY_ITEM_CRS.clone())] sedona_type: SedonaType,
+    ) {
+        let tester = ScalarUdfTester::new(
+            st_set_crs_udf().into(),
+            vec![sedona_type.clone(), SedonaType::Arrow(DataType::Utf8)],
+        );
+        tester.assert_return_type(WKB_GEOGRAPHY_ITEM_CRS.clone());
+
+        // Test with all geographic CRSes (should succeed)
+        let geography_array = create_array(
+            &[
+                Some("POINT (0 1)"),
+                Some("POINT (2 3)"),
+                Some("POINT (4 5)"),
+                Some("POINT (6 7)"),
+            ],
+            &sedona_type,
+        );
+        let geographic_crs_array = create_array!(
+            Utf8,
+            [
+                Some("EPSG:4326"),
+                Some("EPSG:4269"), // NAD83 - geographic
+                Some("0"),         // unset
+                None
+            ]
+        ) as ArrayRef;
+
+        let result = tester
+            .invoke_array_array(geography_array.clone(), geographic_crs_array)
+            .unwrap();
+        assert_eq!(
+            &result,
+            &create_array_item_crs(
+                &[
+                    Some("POINT (0 1)"),
+                    Some("POINT (2 3)"),
+                    Some("POINT (4 5)"),
+                    None
+                ],
+                [Some("OGC:CRS84"), Some("EPSG:4269"), None, None],
+                &WKB_GEOGRAPHY
+            )
+        );
+
+        // Test with a projected CRS (should fail for geography)
+        let projected_crs_array = create_array!(
+            Utf8,
+            [
+                Some("EPSG:4326"),
+                Some("EPSG:3857"), // Web Mercator - projected, should fail
+                Some("EPSG:4326"),
+                Some("EPSG:4326")
+            ]
+        ) as ArrayRef;
+
+        let err = tester
+            .invoke_array_array(geography_array, projected_crs_array)
+            .unwrap_err();
+        assert!(
+            err.message().contains("Can't assign non-geographic CRS"),
+            "Expected error about non-geographic CRS, got: {}",
+            err.message()
+        );
+    }
+
     fn call_udf(
         udf: &ScalarUDF,
         arg: ColumnarValue,
diff --git a/rust/sedona-geometry/src/transform.rs b/rust/sedona-geometry/src/transform.rs
index d8d141a53..159aa9e9a 100644
--- a/rust/sedona-geometry/src/transform.rs
+++ b/rust/sedona-geometry/src/transform.rs
@@ -41,6 +41,7 @@ use geo_traits::{
 
 /// Represents a coordinate reference system (CRS) transformation engine.
 pub trait CrsEngine: Debug {
+    /// Resolve a transform from a source CRS to a destination CRS
     fn get_transform_crs_to_crs(
         &self,
         from: &str,
@@ -48,6 +49,12 @@ pub trait CrsEngine: Debug {
         area_of_interest: Option<BoundingBox>,
         options: &str,
     ) -> Result<Rc<dyn CrsTransform>, SedonaGeometryError>;
+
+    /// Resolve a transform where the transform is represented by a pipeline string
+    ///
+    /// The string accepted varies by engine; however, the PROJ engine typically accepts
+    /// strings in the form of an identifier (for a coordinate transform, not a CRS) or
+    /// PROJ4-ish string.
     fn get_transform_pipeline(
         &self,
         pipeline: &str,
diff --git a/rust/sedona-schema/src/crs.rs b/rust/sedona-schema/src/crs.rs
index fa24602ce..fa5fd2820 100644
--- a/rust/sedona-schema/src/crs.rs
+++ b/rust/sedona-schema/src/crs.rs
@@ -335,6 +335,34 @@ pub trait CoordinateReferenceSystem: Debug {
     /// not be escaped. This is the representation expected as input to PROJ, GDAL,
     /// and Parquet GEOMETRY/GEOGRAPHY representations of CRS.
     fn to_crs_string(&self) -> String;
+
+    /// For Geographic CRSes, return the geographic parameters
+    ///
+    /// For non-geographic CRSes, this must return None. For the purposes of this
+    /// trait, a geographic CRS must be suitable for the Geography type (i.e.,
+    /// units in degrees).
+    fn geographic_params(&self) -> Result<Option<GeographicCrsParams>>;
+}
+
+/// Parameters describing a geographic CRS
+///
+/// These parameters are used for distance calculations on the ellipsoid or sphere
+/// for functions that support it. Notably, this is used for calculations involving
+/// distance in the geography type. Currently this only supports a single parameter
+/// (average spherical radius) but can be expanded to support ellipsoidal parameters
+/// when we have functions that support it.
+pub struct GeographicCrsParams {
+    spherical_radius_m: f64,
+}
+
+impl GeographicCrsParams {
+    /// The average spherical radius in meters for use in spherical geographies
+    ///
+    /// Because geographies with spherical edges approximate distance on the sphere,
+    /// this is the value that will be used to calculate distance in such a case.
+    pub fn spherical_radius(&self) -> f64 {
+        self.spherical_radius_m
+    }
 }
 
 /// Concrete implementation of a default longitude/latitude coordinate reference system
@@ -465,6 +493,40 @@ impl CoordinateReferenceSystem for AuthorityCode {
     fn to_crs_string(&self) -> String {
         self.auth_code.clone()
     }
+
+    /// 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),
+        }
+    }
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -546,6 +608,78 @@ impl CoordinateReferenceSystem for ProjJSON {
     fn to_crs_string(&self) -> String {
         self.to_json()
     }
+
+    fn geographic_params(&self) -> Result<Option<GeographicCrsParams>> {
+        let Value::Object(obj) = &self.value else {
+            return Ok(None);
+        };
+
+        let Some(Value::String(crs_type)) = obj.get("type") else {
+            return Ok(None);
+        };
+
+        // Only geographic CRSes have geographic params
+        if crs_type != "GeographicCRS" {
+            return Ok(None);
+        }
+
+        // Try datum first, then datum_ensemble
+        let ellipsoid = if let Some(Value::Object(datum)) = obj.get("datum") {
+            datum.get("ellipsoid")
+        } else if let Some(Value::Object(datum_ensemble)) = obj.get("datum_ensemble") {
+            datum_ensemble.get("ellipsoid")
+        } else {
+            return exec_err!(
+                "PROJJSON GeographicCRS missing or malformed datum or datum_ensemble ellipsoid"
+            );
+        };
+
+        let Some(Value::Object(ellipsoid)) = ellipsoid else {
+            return exec_err!("PROJJSON ellipsoid is not an object");
+        };
+
+        // Handle spherical case (radius only)
+        if let Some(Value::Number(radius)) = ellipsoid.get("radius") {
+            let Some(r) = radius.as_f64() else {
+                return exec_err!("PROJJSON radius can't be converted to f64");
+            };
+            return Ok(Some(GeographicCrsParams {
+                spherical_radius_m: r,
+            }));
+        }
+
+        // Get semi_major_axis (required for ellipsoidal case)
+        let Some(Value::Number(semi_major)) = ellipsoid.get("semi_major_axis") else {
+            return exec_err!("PROJJSON ellipsoid missing or malformed semi_major_axis");
+        };
+        let Some(a) = semi_major.as_f64() else {
+            return exec_err!("PROJJSON semi_major_axis can't be converted to f64");
+        };
+
+        // Calculate semi_minor_axis from inverse_flattening or use directly
+        let b = if let Some(Value::Number(inv_f)) = ellipsoid.get("inverse_flattening") {
+            if let Some(inv_f) = inv_f.as_f64() {
+                a * (1.0 - 1.0 / inv_f)
+            } else {
+                return exec_err!("PROJJSON inverse_flattening can't be converted to f64");
+            }
+        } else if let Some(Value::Number(semi_minor)) = ellipsoid.get("semi_minor_axis") {
+            if let Some(b) = semi_minor.as_f64() {
+                b
+            } else {
+                return exec_err!("PROJJSON semi_minor_axis can't be converted to f64");
+            }
+        } else {
+            return exec_err!(
+                "PROJJSON ellipsoid missing or malformed inverse_flattening/semi_minor_axis"
+            );
+        };
+
+        // Mean radius = (2a + b) / 3
+        Ok(Some(GeographicCrsParams {
+            spherical_radius_m: (2.0 * a + b) / 3.0,
+        }))
+    }
 }
 
 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  [...]
@@ -553,8 +687,15 @@ pub const OGC_CRS84_PROJJSON: &str = r#"{"$schema":"https://proj.org/schemas/v0.
 #[cfg(test)]
 mod test {
     use super::*;
+    /// Projected CRS
     const EPSG_6318_PROJJSON: &str = r#"{"$schema": "https://proj.org/schemas/v0.4/projjson.schema.json","type": "GeographicCRS","name": "NAD83(2011)","datum": {"type": "GeodeticReferenceFrame","name": "NAD83 (National Spatial Reference System 2011)","ellipsoid": {"name": "GRS 1980","semi_major_axis": 6378137,"inverse_flattening": 298.257222101}},"coordinate_system": {"subtype": "ellipsoidal","axis": [{"name": "Geodetic latitude","abbreviation": "Lat","direction": "north","unit": "degree [...]
 
+    /// Mars geographic CRS (only defines 'radius')
+    const IAU_49900_PROJJSON: &str = r#"{"$schema":"https://proj.org/schemas/v0.7/projjson.schema.json","type":"GeographicCRS","name":"Mars (2015) - Sphere / Ocentric","datum":{"type":"GeodeticReferenceFrame","name":"Mars (2015) - Sphere","anchor":"Viking 1 lander: 47.95137 W","ellipsoid":{"name":"Mars (2015) - Sphere","radius":3396190},"prime_meridian":{"name":"Reference Meridian","longitude":0}},"coordinate_system":{"subtype":"ellipsoidal","axis":[{"name":"Geodetic latitude","abbreviat [...]
+
+    /// NAD27 (defines semi_major/semi_minor instead of inverse flattening)
+    const EPSG_4267_PROJJSON: &str = r#"{"$schema":"https://proj.org/schemas/v0.7/projjson.schema.json","type":"GeographicCRS","name":"NAD27","datum":{"type":"GeodeticReferenceFrame","name":"North American Datum 1927","ellipsoid":{"name":"Clarke 1866","semi_major_axis":6378206.4,"semi_minor_axis":6356583.8}},"coordinate_system":{"subtype":"ellipsoidal","axis":[{"name":"Geodetic latitude","abbreviation":"Lat","direction":"north","unit":"degree"},{"name":"Geodetic longitude","abbreviation" [...]
+
     #[test]
     fn deserialize() {
         // lnglat() crses
@@ -694,4 +835,133 @@ mod test {
         let new_crs = deserialize_crs_from_obj(&crs_value).unwrap();
         assert_eq!(new_crs.clone().unwrap().srid().unwrap(), Some(4269));
     }
+
+    #[test]
+    fn geographic_params() {
+        let lnglat = AuthorityCode {
+            auth_code: "OGC:CRS84".to_string(),
+        };
+        let params = lnglat.geographic_params().unwrap().unwrap();
+        assert_eq!(params.spherical_radius(), 6371010.0);
+
+        let auth_code_nad27 = AuthorityCode {
+            auth_code: "EPSG:4267".to_string(),
+        };
+        let params = auth_code_nad27.geographic_params().unwrap().unwrap();
+        assert_eq!(params.spherical_radius(), 6370998.866666667);
+
+        let projjson_nad27 = ProjJSON::from_str(EPSG_4267_PROJJSON).unwrap();
+        let params = projjson_nad27.geographic_params().unwrap().unwrap();
+        assert_eq!(params.spherical_radius(), 6370998.866666667);
+
+        let projjson_wgs84 = ProjJSON::from_str(OGC_CRS84_PROJJSON).unwrap();
+        let params = projjson_wgs84.geographic_params().unwrap().unwrap();
+        assert_eq!(params.spherical_radius(), 6371008.771415059);
+
+        let projjson_mars = ProjJSON::from_str(IAU_49900_PROJJSON).unwrap();
+        let params = projjson_mars.geographic_params().unwrap().unwrap();
+        assert_eq!(params.spherical_radius(), 3396190.0);
+    }
+
+    #[test]
+    fn geographic_params_invalid_projjson() {
+        // Missing type field -> Ok(None)
+        let no_type = ProjJSON::try_new(serde_json::json!({"name": "test"})).unwrap();
+        assert!(no_type.geographic_params().unwrap().is_none());
+
+        // Non-GeographicCRS type -> Ok(None)
+        let projected = ProjJSON::try_new(serde_json::json!({
+            "type": "ProjectedCRS",
+            "name": "test"
+        }))
+        .unwrap();
+        assert!(projected.geographic_params().unwrap().is_none());
+
+        // GeographicCRS missing datum and datum_ensemble -> Error
+        let no_datum = ProjJSON::try_new(serde_json::json!({
+            "type": "GeographicCRS",
+            "name": "test"
+        }))
+        .unwrap();
+        assert!(no_datum.geographic_params().is_err());
+
+        // datum is not an object -> Error (datum_ensemble fallback also fails)
+        let datum_not_object = ProjJSON::try_new(serde_json::json!({
+            "type": "GeographicCRS",
+            "datum": "not an object"
+        }))
+        .unwrap();
+        assert!(datum_not_object.geographic_params().is_err());
+
+        // ellipsoid is not an object -> Error
+        let ellipsoid_not_object = ProjJSON::try_new(serde_json::json!({
+            "type": "GeographicCRS",
+            "datum": {"ellipsoid": "not an object"}
+        }))
+        .unwrap();
+        assert!(ellipsoid_not_object.geographic_params().is_err());
+
+        // ellipsoid missing all axis definitions -> Error
+        let ellipsoid_empty = ProjJSON::try_new(serde_json::json!({
+            "type": "GeographicCRS",
+            "datum": {"ellipsoid": {"name": "empty"}}
+        }))
+        .unwrap();
+        assert!(ellipsoid_empty.geographic_params().is_err());
+
+        // semi_major_axis is not a number -> Error
+        let semi_major_not_number = ProjJSON::try_new(serde_json::json!({
+            "type": "GeographicCRS",
+            "datum": {"ellipsoid": {"semi_major_axis": "not a number"}}
+        }))
+        .unwrap();
+        assert!(semi_major_not_number.geographic_params().is_err());
+
+        // semi_major_axis present but no inverse_flattening or semi_minor_axis -> Error
+        let missing_minor = ProjJSON::try_new(serde_json::json!({
+            "type": "GeographicCRS",
+            "datum": {"ellipsoid": {"semi_major_axis": 6378137}}
+        }))
+        .unwrap();
+        assert!(missing_minor.geographic_params().is_err());
+
+        // inverse_flattening is not a number -> Error
+        let inv_f_not_number = ProjJSON::try_new(serde_json::json!({
+            "type": "GeographicCRS",
+            "datum": {"ellipsoid": {
+                "semi_major_axis": 6378137,
+                "inverse_flattening": "not a number"
+            }}
+        }))
+        .unwrap();
+        assert!(inv_f_not_number.geographic_params().is_err());
+
+        // semi_minor_axis is not a number -> Error
+        let semi_minor_not_number = ProjJSON::try_new(serde_json::json!({
+            "type": "GeographicCRS",
+            "datum": {"ellipsoid": {
+                "semi_major_axis": 6378137,
+                "semi_minor_axis": "not a number"
+            }}
+        }))
+        .unwrap();
+        assert!(semi_minor_not_number.geographic_params().is_err());
+
+        // radius is not a number -> Error
+        let radius_not_number = ProjJSON::try_new(serde_json::json!({
+            "type": "GeographicCRS",
+            "datum": {"ellipsoid": {"radius": "not a number"}}
+        }))
+        .unwrap();
+        assert!(radius_not_number.geographic_params().is_err());
+
+        // datum_ensemble path works when datum is missing
+        let datum_ensemble = ProjJSON::try_new(serde_json::json!({
+            "type": "GeographicCRS",
+            "datum_ensemble": {"ellipsoid": {"radius": 6371000}}
+        }))
+        .unwrap();
+        let params = datum_ensemble.geographic_params().unwrap().unwrap();
+        assert_eq!(params.spherical_radius(), 6371000.0);
+    }
 }
diff --git a/rust/sedona-schema/src/datatypes.rs b/rust/sedona-schema/src/datatypes.rs
index 3bc32350c..46b49b1f2 100644
--- a/rust/sedona-schema/src/datatypes.rs
+++ b/rust/sedona-schema/src/datatypes.rs
@@ -21,7 +21,7 @@ use serde_json::Value;
 use std::fmt::{Debug, Display};
 use std::sync::LazyLock;
 
-use crate::crs::{deserialize_crs, deserialize_crs_from_obj, Crs};
+use crate::crs::{deserialize_crs, deserialize_crs_from_obj, lnglat, Crs};
 use crate::extension_type::ExtensionType;
 use crate::raster::RasterSchema;
 
@@ -67,7 +67,8 @@ pub const WKB_VIEW_GEOMETRY: SedonaType = SedonaType::WkbView(Edges::Planar, Crs
 /// Sentinel for [`SedonaType::Wkb`] with spherical edges
 ///
 /// This constant is useful when defining type signatures as these ignore the Crs when
-/// matching (and `SedonaType::Wkb(...)` is verbose)
+/// matching (and `SedonaType::Wkb(...)` is verbose). Note that [WKB_GEOGRAPHY_WGS84]
+/// is likely more appropriate in many cases.
 pub const WKB_GEOGRAPHY: SedonaType = SedonaType::Wkb(Edges::Spherical, Crs::None);
 
 /// Sentinel for [`SedonaType::WkbView`] with spherical edges
@@ -75,6 +76,20 @@ pub const WKB_GEOGRAPHY: SedonaType = SedonaType::Wkb(Edges::Spherical, Crs::Non
 /// See [`WKB_GEOGRAPHY`]
 pub const WKB_VIEW_GEOGRAPHY: SedonaType = SedonaType::WkbView(Edges::Spherical, Crs::None);
 
+/// Sentinel for [`SedonaType::Wkb`] with spherical edges and longitude, latitude CRS
+///
+/// This constant is useful when defining type signatures as these ignore the Crs when
+/// matching (and `SedonaType::Wkb(...)` is verbose)
+pub static WKB_GEOGRAPHY_WGS84: LazyLock<SedonaType> =
+    LazyLock::new(|| SedonaType::Wkb(Edges::Spherical, lnglat()));
+
+/// Sentinel for [`SedonaType::WkbView`] with spherical edges and longitude, latitude CRS
+///
+/// This constant is useful when defining type signatures as these ignore the Crs when
+/// matching (and `SedonaType::WkbView(...)` is verbose)
+pub static WKB_VIEW_GEOGRAPHY_WGS84: LazyLock<SedonaType> =
+    LazyLock::new(|| SedonaType::WkbView(Edges::Spherical, lnglat()));
+
 /// Sentinel for [`SedonaType::Raster`]
 pub const RASTER: SedonaType = SedonaType::Raster;