You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ja...@apache.org on 2026/09/17 21:08:56 UTC

(sedona-db) branch main updated: feat(rust/sedona-raster-functions): RS_WorldToRasterCoord(X/Y) point-geometry overload (#1314)

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

james-willis 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 22623e6c2 feat(rust/sedona-raster-functions): RS_WorldToRasterCoord(X/Y) point-geometry overload (#1314)
22623e6c2 is described below

commit 22623e6c2d602127bf08a165527719ef321cf991
Author: James Willis <ja...@users.noreply.github.com>
AuthorDate: Thu Sep 17 14:08:49 2026 -0700

    feat(rust/sedona-raster-functions): RS_WorldToRasterCoord(X/Y) point-geometry overload (#1314)
---
 docs/reference/sql/rs_worldtorastercoord.qmd       |  14 ++
 docs/reference/sql/rs_worldtorastercoordx.qmd      |  14 ++
 docs/reference/sql/rs_worldtorastercoordy.qmd      |  14 ++
 .../spark-parity/test_rs_worldtorastercoord.py     |  15 +-
 .../spark-parity/test_rs_worldtorastercoordx.py    |  16 +-
 .../spark-parity/test_rs_worldtorastercoordy.py    |  16 +-
 .../tests/functions/test_raster_functions.py       |  33 +++
 .../src/rs_rastercoordinate.rs                     | 238 ++++++++++++++++-----
 8 files changed, 278 insertions(+), 82 deletions(-)

diff --git a/docs/reference/sql/rs_worldtorastercoord.qmd b/docs/reference/sql/rs_worldtorastercoord.qmd
index 40aa9c824..5fc4f2cd3 100644
--- a/docs/reference/sql/rs_worldtorastercoord.qmd
+++ b/docs/reference/sql/rs_worldtorastercoord.qmd
@@ -26,6 +26,14 @@ kernels:
       type: double
     - name: "y"
       type: double
+  - returns: geometry
+    args:
+    - raster
+    - name: point
+      type: geometry
+      description: >
+        The world coordinate is read from this point. Its coordinates are used
+        as given, in the raster's own space — this form does not reproject.
 ---
 
 ## Examples
@@ -33,3 +41,9 @@ kernels:
 ```sql
 SELECT RS_WorldToRasterCoord(RS_Example(), 10.0, 20.0);
 ```
+
+The world coordinate can also be given as a point:
+
+```sql
+SELECT RS_WorldToRasterCoord(RS_Example(), ST_Point(10.0, 20.0));
+```
diff --git a/docs/reference/sql/rs_worldtorastercoordx.qmd b/docs/reference/sql/rs_worldtorastercoordx.qmd
index 322156bc1..f667a35b2 100644
--- a/docs/reference/sql/rs_worldtorastercoordx.qmd
+++ b/docs/reference/sql/rs_worldtorastercoordx.qmd
@@ -26,6 +26,14 @@ kernels:
       type: double
     - name: "y"
       type: double
+  - returns: integer
+    args:
+    - raster
+    - name: point
+      type: geometry
+      description: >
+        The world coordinate is read from this point. Its coordinates are used
+        as given, in the raster's own space — this form does not reproject.
 ---
 
 ## Examples
@@ -33,3 +41,9 @@ kernels:
 ```sql
 SELECT RS_WorldToRasterCoordX(RS_Example(), 10.0, 20.0);
 ```
+
+The world coordinate can also be given as a point:
+
+```sql
+SELECT RS_WorldToRasterCoordX(RS_Example(), ST_Point(10.0, 20.0));
+```
diff --git a/docs/reference/sql/rs_worldtorastercoordy.qmd b/docs/reference/sql/rs_worldtorastercoordy.qmd
index c69017121..a0588a704 100644
--- a/docs/reference/sql/rs_worldtorastercoordy.qmd
+++ b/docs/reference/sql/rs_worldtorastercoordy.qmd
@@ -26,6 +26,14 @@ kernels:
       type: double
     - name: "y"
       type: double
+  - returns: integer
+    args:
+    - raster
+    - name: point
+      type: geometry
+      description: >
+        The world coordinate is read from this point. Its coordinates are used
+        as given, in the raster's own space — this form does not reproject.
 ---
 
 ## Examples
@@ -33,3 +41,9 @@ kernels:
 ```sql
 SELECT RS_WorldToRasterCoordY(RS_Example(), 10.0, 20.0);
 ```
+
+The world coordinate can also be given as a point:
+
+```sql
+SELECT RS_WorldToRasterCoordY(RS_Example(), ST_Point(10.0, 20.0));
+```
diff --git a/integration/spark-parity/test_rs_worldtorastercoord.py b/integration/spark-parity/test_rs_worldtorastercoord.py
index 1bfd39765..bbd9691a3 100644
--- a/integration/spark-parity/test_rs_worldtorastercoord.py
+++ b/integration/spark-parity/test_rs_worldtorastercoord.py
@@ -23,10 +23,9 @@ case is a cataloged divergence: SedonaDB treats pixel coordinates as
 0-based where Sedona Spark (following PostGIS, and SedonaDB's own
 RS_PixelAs* functions) is 1-based (apache/sedona-db#1235) — and on a
 fractional negative index the engines also disagree on rounding, so
-outside the grid the answers are not even a uniform pixel apart.
-Sedona Spark additionally accepts a point geometry in place of the
-(x, y) pair, an overload SedonaDB lacks (the X/Y variants share both
-gaps).
+outside the grid the answers are not even a uniform pixel apart. Both
+engines also accept a point geometry in place of the (x, y) pair, and
+that form carries the same 0- vs 1-based divergence.
 """
 
 import pytest
@@ -79,10 +78,10 @@ def test_rs_worldtorastercoord_outside(tmp_path):
 
 
 @pytest.mark.xfail(
-    reason="SedonaDB has no point-geometry overload of RS_WorldToRasterCoord "
-    "(it raises 'No kernel matching arguments'); Sedona Spark accepts "
-    "(raster, point) and answers 1-based, so once the overload exists the "
-    "0- vs 1-based divergence (apache/sedona-db#1235) still applies"
+    reason="both engines now accept the (raster, point) overload, but SedonaDB "
+    "reads pixel coordinates 0-based where Sedona Spark is 1-based "
+    "(apache/sedona-db#1235), so the point form lands one pixel apart, exactly "
+    "like the numeric form"
 )
 def test_rs_worldtorastercoord_point_overload(tmp_path):
     """The point-geometry form maps the interior point (104 494) to the
diff --git a/integration/spark-parity/test_rs_worldtorastercoordx.py b/integration/spark-parity/test_rs_worldtorastercoordx.py
index 7376db3c4..1ee057221 100644
--- a/integration/spark-parity/test_rs_worldtorastercoordx.py
+++ b/integration/spark-parity/test_rs_worldtorastercoordx.py
@@ -19,11 +19,9 @@
 Every case is a cataloged divergence: SedonaDB treats pixel coordinates
 as 0-based where Sedona Spark (following PostGIS, and SedonaDB's own
 RS_PixelAs* functions) is 1-based, so results are exactly one pixel
-apart everywhere, extrapolation included (apache/sedona-db#1235). Sedona
-Spark also accepts a point geometry in place of the (x, y) pair, an
-overload SedonaDB lacks entirely. The geometry-returning combined form
-is deferred until the harness can compare geometry columns without ST_
-wrappers.
+apart everywhere, extrapolation included (apache/sedona-db#1235). Both
+engines also accept a point geometry in place of the (x, y) pair, and
+that form carries the same 0- vs 1-based divergence.
 """
 
 import pytest
@@ -50,10 +48,10 @@ def test_rs_worldtorastercoordx(x, y, tmp_path):
 
 
 @pytest.mark.xfail(
-    reason="SedonaDB has no point-geometry overload of RS_WorldToRasterCoordX "
-    "(it raises 'No kernel matching arguments'); Sedona Spark accepts "
-    "(raster, point) and answers 1-based, so once the overload exists the "
-    "0- vs 1-based divergence (apache/sedona-db#1235) still applies"
+    reason="both engines now accept the (raster, point) overload, but SedonaDB "
+    "reads pixel coordinates 0-based where Sedona Spark is 1-based "
+    "(apache/sedona-db#1235), so the point form diverges by one column, exactly "
+    "like the numeric form"
 )
 def test_rs_worldtorastercoordx_point_overload(tmp_path):
     """The point-geometry form maps the interior point (104 494) to the
diff --git a/integration/spark-parity/test_rs_worldtorastercoordy.py b/integration/spark-parity/test_rs_worldtorastercoordy.py
index a49014c15..a7ac96ef9 100644
--- a/integration/spark-parity/test_rs_worldtorastercoordy.py
+++ b/integration/spark-parity/test_rs_worldtorastercoordy.py
@@ -19,11 +19,9 @@
 Every case is a cataloged divergence: SedonaDB treats pixel coordinates
 as 0-based where Sedona Spark (following PostGIS, and SedonaDB's own
 RS_PixelAs* functions) is 1-based, so results are exactly one pixel
-apart everywhere, extrapolation included (apache/sedona-db#1235). Sedona
-Spark also accepts a point geometry in place of the (x, y) pair, an
-overload SedonaDB lacks entirely. The geometry-returning combined form
-is deferred until the harness can compare geometry columns without ST_
-wrappers.
+apart everywhere, extrapolation included (apache/sedona-db#1235). Both
+engines also accept a point geometry in place of the (x, y) pair, and
+that form carries the same 0- vs 1-based divergence.
 """
 
 import pytest
@@ -46,10 +44,10 @@ def test_rs_worldtorastercoordy(tmp_path):
 
 
 @pytest.mark.xfail(
-    reason="SedonaDB has no point-geometry overload of RS_WorldToRasterCoordY "
-    "(it raises 'No kernel matching arguments'); Sedona Spark accepts "
-    "(raster, point) and answers 1-based, so once the overload exists the "
-    "0- vs 1-based divergence (apache/sedona-db#1235) still applies"
+    reason="both engines now accept the (raster, point) overload, but SedonaDB "
+    "reads pixel coordinates 0-based where Sedona Spark is 1-based "
+    "(apache/sedona-db#1235), so the point form diverges by one row, exactly "
+    "like the numeric form"
 )
 def test_rs_worldtorastercoordy_point_overload(tmp_path):
     """The point-geometry form maps the interior point (104 494) to the
diff --git a/python/sedonadb/tests/functions/test_raster_functions.py b/python/sedonadb/tests/functions/test_raster_functions.py
index adac69ee3..6bf939c0c 100644
--- a/python/sedonadb/tests/functions/test_raster_functions.py
+++ b/python/sedonadb/tests/functions/test_raster_functions.py
@@ -150,6 +150,39 @@ def test_rs_value_default_band_requires_single_band(con):
         ).to_arrow_table()
 
 
+# RS_WorldToRasterCoord and its X/Y variants map a world coordinate into the
+# raster's pixel space. The two argument forms — a pair of ordinates, or a
+# single point geometry — are the same mapping, so both are exercised against
+# the same expectation. RS_Example is skewed (scale 2, skew 1), and (10, 20)
+# maps outside the grid: the mapping extrapolates rather than clamping.
+@pytest.mark.parametrize(
+    "coord",
+    [
+        pytest.param("10.0, 20.0", id="ordinates"),
+        pytest.param("ST_Point(10.0, 20.0)", id="point"),
+    ],
+)
+@pytest.mark.parametrize(
+    ("fn", "expected"),
+    [
+        ("RS_WorldToRasterCoordX", -2),
+        ("RS_WorldToRasterCoordY", -28),
+        ("RS_WorldToRasterCoord", "POINT (-2 -28)"),
+    ],
+)
+def test_rs_worldtorastercoord(fn, expected, coord):
+    SedonaDB().assert_query_result(f"SELECT {fn}(RS_Example(), {coord})", expected)
+
+
+def test_rs_worldtorastercoord_empty_point_is_null():
+    # POINT EMPTY has no location to map, so it yields NULL rather than an
+    # error — matching how the two-ordinate form treats a null ordinate.
+    SedonaDB().assert_query_result(
+        "SELECT RS_WorldToRasterCoordX(RS_Example(), ST_GeomFromText('POINT EMPTY'))",
+        None,
+    )
+
+
 def test_rs_value_matches_rasterio(con):
     """Cross-check RS_Value against rasterio on a random raster.
 
diff --git a/rust/sedona-raster-functions/src/rs_rastercoordinate.rs b/rust/sedona-raster-functions/src/rs_rastercoordinate.rs
index 59e688074..e0024b395 100644
--- a/rust/sedona-raster-functions/src/rs_rastercoordinate.rs
+++ b/rust/sedona-raster-functions/src/rs_rastercoordinate.rs
@@ -17,15 +17,21 @@
 use std::{sync::Arc, vec};
 
 use crate::executor::RasterExecutor;
+use arrow_array::Float64Array;
 use arrow_array::builder::{BinaryBuilder, Int64Builder};
 use arrow_schema::DataType;
 use datafusion_common::cast::as_float64_array;
 use datafusion_common::error::Result;
+use datafusion_common::exec_datafusion_err;
 use datafusion_expr::{ColumnarValue, Volatility};
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
 use sedona_geometry::types::Edges;
+use sedona_geometry::wkb_header::read_point_xy;
 use sedona_raster::affine_transformation::to_raster_coordinate;
-use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
+use sedona_schema::{
+    datatypes::SedonaType,
+    matchers::{ArgMatcher, TypeMatcher},
+};
 
 /// RS_WorldToRasterCoordY() scalar UDF documentation
 ///
@@ -33,7 +39,16 @@ use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
 pub fn rs_worldtorastercoordy_udf() -> SedonaScalarUDF {
     SedonaScalarUDF::new(
         "rs_worldtorastercoordy",
-        vec![Arc::new(RsCoordinateMapper { coord: Coord::Y })],
+        vec![
+            Arc::new(RsCoordinateMapper {
+                coord: Coord::Y,
+                from_point: false,
+            }),
+            Arc::new(RsCoordinateMapper {
+                coord: Coord::Y,
+                from_point: true,
+            }),
+        ],
         Volatility::Immutable,
     )
 }
@@ -44,7 +59,16 @@ pub fn rs_worldtorastercoordy_udf() -> SedonaScalarUDF {
 pub fn rs_worldtorastercoordx_udf() -> SedonaScalarUDF {
     SedonaScalarUDF::new(
         "rs_worldtorastercoordx",
-        vec![Arc::new(RsCoordinateMapper { coord: Coord::X })],
+        vec![
+            Arc::new(RsCoordinateMapper {
+                coord: Coord::X,
+                from_point: false,
+            }),
+            Arc::new(RsCoordinateMapper {
+                coord: Coord::X,
+                from_point: true,
+            }),
+        ],
         Volatility::Immutable,
     )
 }
@@ -55,7 +79,10 @@ pub fn rs_worldtorastercoordx_udf() -> SedonaScalarUDF {
 pub fn rs_worldtorastercoord_udf() -> SedonaScalarUDF {
     SedonaScalarUDF::new(
         "rs_worldtorastercoord",
-        vec![Arc::new(RsCoordinatePoint {})],
+        vec![
+            Arc::new(RsCoordinatePoint { from_point: false }),
+            Arc::new(RsCoordinatePoint { from_point: true }),
+        ],
         Volatility::Immutable,
     )
 }
@@ -66,22 +93,30 @@ enum Coord {
     Y,
 }
 
+impl Coord {
+    fn pick(&self, raster_x: i64, raster_y: i64) -> i64 {
+        match self {
+            Coord::X => raster_x,
+            Coord::Y => raster_y,
+        }
+    }
+}
+
 #[derive(Debug)]
 struct RsCoordinateMapper {
     coord: Coord,
+    /// `false`: `(raster, worldX, worldY)`. `true`: the Sedona Spark parity
+    /// overload `(raster, point)`, which reads the world coordinate from a
+    /// point geometry.
+    from_point: bool,
 }
 
 impl SedonaScalarKernel for RsCoordinateMapper {
     fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
         let matcher = ArgMatcher::new(
-            vec![
-                ArgMatcher::is_raster(),
-                ArgMatcher::is_numeric(),
-                ArgMatcher::is_numeric(),
-            ],
+            coord_input_matchers(self.from_point),
             SedonaType::Arrow(DataType::Int64),
         );
-
         matcher.match_args(args)
     }
 
@@ -93,29 +128,12 @@ impl SedonaScalarKernel for RsCoordinateMapper {
         let executor = RasterExecutor::new(arg_types, args);
         let mut builder = Int64Builder::with_capacity(executor.num_iterations());
 
-        // Expand world x and y coordinate parameters to arrays and cast to Float64
-        let world_x_array = args[1].clone().cast_to(&DataType::Float64, None)?;
-        let world_x_array = world_x_array.into_array(executor.num_iterations())?;
-        let world_x_array = as_float64_array(&world_x_array)?;
-        let world_y_array = args[2].clone().cast_to(&DataType::Float64, None)?;
-        let world_y_array = world_y_array.into_array(executor.num_iterations())?;
-        let world_y_array = as_float64_array(&world_y_array)?;
-        let mut world_x_iter = world_x_array.iter();
-        let mut world_y_iter = world_y_array.iter();
-
-        executor.execute_raster_void(|_i, raster_opt| {
-            let x_opt = world_x_iter.next().unwrap();
-            let y_opt = world_y_iter.next().unwrap();
-
-            match (raster_opt, x_opt, y_opt) {
-                (Some(raster), Some(x), Some(y)) => {
-                    let (raster_x, raster_y) = to_raster_coordinate(raster, x, y)?;
-                    match self.coord {
-                        Coord::X => builder.append_value(raster_x),
-                        Coord::Y => builder.append_value(raster_y),
-                    };
+        visit_raster_coordinates(&executor, args, self.from_point, |coord| {
+            match coord {
+                Some((raster_x, raster_y)) => {
+                    builder.append_value(self.coord.pick(raster_x, raster_y))
                 }
-                (_, _, _) => builder.append_null(),
+                None => builder.append_null(),
             }
             Ok(())
         })?;
@@ -125,15 +143,15 @@ impl SedonaScalarKernel for RsCoordinateMapper {
 }
 
 #[derive(Debug)]
-struct RsCoordinatePoint;
+struct RsCoordinatePoint {
+    /// `false`: `(raster, worldX, worldY)`. `true`: the Sedona Spark parity
+    /// overload `(raster, point)`.
+    from_point: bool,
+}
 impl SedonaScalarKernel for RsCoordinatePoint {
     fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
         let matcher = ArgMatcher::new(
-            vec![
-                ArgMatcher::is_raster(),
-                ArgMatcher::is_numeric(),
-                ArgMatcher::is_numeric(),
-            ],
+            coord_input_matchers(self.from_point),
             SedonaType::Wkb(Edges::Planar, None),
         );
 
@@ -154,28 +172,14 @@ impl SedonaScalarKernel for RsCoordinatePoint {
             item.len() * executor.num_iterations(),
         );
 
-        // Expand world x and y coordinate parameters to arrays and cast to Float64
-        let world_x_array = args[1].clone().cast_to(&DataType::Float64, None)?;
-        let world_x_array = world_x_array.into_array(executor.num_iterations())?;
-        let world_x_array = as_float64_array(&world_x_array)?;
-        let world_y_array = args[2].clone().cast_to(&DataType::Float64, None)?;
-        let world_y_array = world_y_array.into_array(executor.num_iterations())?;
-        let world_y_array = as_float64_array(&world_y_array)?;
-        let mut world_x_iter = world_x_array.iter();
-        let mut world_y_iter = world_y_array.iter();
-
-        executor.execute_raster_void(|_i, raster_opt| {
-            let x_opt = world_x_iter.next().unwrap();
-            let y_opt = world_y_iter.next().unwrap();
-
-            match (raster_opt, x_opt, y_opt) {
-                (Some(raster), Some(world_x), Some(world_y)) => {
-                    let (raster_x, raster_y) = to_raster_coordinate(raster, world_x, world_y)?;
+        visit_raster_coordinates(&executor, args, self.from_point, |coord| {
+            match coord {
+                Some((raster_x, raster_y)) => {
                     item[5..13].copy_from_slice(&(raster_x as f64).to_le_bytes());
                     item[13..21].copy_from_slice(&(raster_y as f64).to_le_bytes());
                     builder.append_value(item);
                 }
-                (_, _, _) => builder.append_null(),
+                None => builder.append_null(),
             }
             Ok(())
         })?;
@@ -184,6 +188,91 @@ impl SedonaScalarKernel for RsCoordinatePoint {
     }
 }
 
+/// Visit each row's world coordinate mapped into the raster's `(col, row)`
+/// space, from whichever input form the kernel takes.
+///
+/// This is the whole difference between the two input forms — the numeric
+/// `(raster, worldX, worldY)` and the `(raster, point)` overload — so the
+/// kernels differ only in what they build from the result. `visit` receives
+/// `None` for a row with no coordinate to map: a null raster, a null or empty
+/// point, or a null ordinate.
+fn visit_raster_coordinates(
+    executor: &RasterExecutor,
+    args: &[ColumnarValue],
+    from_point: bool,
+    mut visit: impl FnMut(Option<(i64, i64)>) -> Result<()>,
+) -> Result<()> {
+    if from_point {
+        executor.execute_raster_wkb_crs_void(|raster_opt, wkb_opt, _crs| {
+            match (raster_opt, point_xy(wkb_opt)?) {
+                (Some(raster), Some((x, y))) => visit(Some(to_raster_coordinate(raster, x, y)?)),
+                _ => visit(None),
+            }
+        })
+    } else {
+        let (world_x, world_y) = world_xy_arrays(args, executor.num_iterations())?;
+        let mut world_x_iter = world_x.iter();
+        let mut world_y_iter = world_y.iter();
+        executor.execute_raster_void(|_i, raster_opt| {
+            // Both ordinate iterators advance every row, null raster included,
+            // so they stay in lockstep with the row index.
+            match (
+                raster_opt,
+                world_x_iter.next().unwrap(),
+                world_y_iter.next().unwrap(),
+            ) {
+                (Some(raster), Some(x), Some(y)) => {
+                    visit(Some(to_raster_coordinate(raster, x, y)?))
+                }
+                (_, _, _) => visit(None),
+            }
+        })
+    }
+}
+
+/// Input matchers for a world→raster coordinate function: the numeric
+/// `(raster, worldX, worldY)` form, or the `(raster, point)` overload.
+fn coord_input_matchers(from_point: bool) -> Vec<Arc<dyn TypeMatcher + Send + Sync>> {
+    if from_point {
+        vec![
+            ArgMatcher::is_raster(),
+            ArgMatcher::is_geometry_or_geography(),
+        ]
+    } else {
+        vec![
+            ArgMatcher::is_raster(),
+            ArgMatcher::is_numeric(),
+            ArgMatcher::is_numeric(),
+        ]
+    }
+}
+
+/// The world `(x, y)` carried by a point geometry, or `None` for a null or
+/// empty point. Errors on a non-point geometry. The point's coordinates are
+/// used as-is, in the raster's own space — the numeric form takes no CRS
+/// either, so the point overload does not reproject.
+fn point_xy(wkb_opt: Option<&[u8]>) -> Result<Option<(f64, f64)>> {
+    match wkb_opt {
+        Some(wkb) => {
+            read_point_xy(wkb).map_err(|e| exec_datafusion_err!("RS_WorldToRasterCoord: {e}"))
+        }
+        None => Ok(None),
+    }
+}
+
+/// The world X and Y numeric arguments (args 1 and 2) as Float64 row arrays.
+fn world_xy_arrays(args: &[ColumnarValue], n: usize) -> Result<(Float64Array, Float64Array)> {
+    let x = args[1]
+        .clone()
+        .cast_to(&DataType::Float64, None)?
+        .into_array(n)?;
+    let y = args[2]
+        .clone()
+        .cast_to(&DataType::Float64, None)?
+        .into_array(n)?;
+    Ok((as_float64_array(&x)?.clone(), as_float64_array(&y)?.clone()))
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -254,6 +343,43 @@ mod tests {
         );
     }
 
+    #[rstest]
+    fn udf_invoke_xy_from_point(#[values(Coord::Y, Coord::X)] coord: Coord) {
+        // The (raster, point) overload reads the world coordinate from a point
+        // geometry and answers exactly like the numeric form: world (2, 3) is
+        // raster 1's origin -> raster coordinate (0, 0). Raster 0 is nulled.
+        let udf = match coord {
+            Coord::X => rs_worldtorastercoordx_udf(),
+            Coord::Y => rs_worldtorastercoordy_udf(),
+        };
+        let tester = ScalarUdfTester::new(udf.into(), vec![RASTER, WKB_GEOMETRY]);
+        let rasters = generate_test_rasters(2, Some(0)).unwrap();
+        let points = create_array(&[Some("POINT (2 3)"), Some("POINT (2 3)")], &WKB_GEOMETRY);
+        let expected: Arc<dyn arrow_array::Array> =
+            Arc::new(arrow_array::Int64Array::from(vec![None, Some(0_i64)]));
+        let result = tester
+            .invoke_array_array(Arc::new(rasters), points)
+            .unwrap();
+        assert_array_equal(&result, &expected);
+    }
+
+    #[rstest]
+    fn udf_invoke_pt_from_point() {
+        // The combined (raster, point) overload returns the raster-coordinate
+        // point, matching the numeric form: world (2, 3) -> POINT (0 0).
+        let tester = ScalarUdfTester::new(
+            rs_worldtorastercoord_udf().into(),
+            vec![RASTER, WKB_GEOMETRY],
+        );
+        let rasters = generate_test_rasters(2, Some(0)).unwrap();
+        let points = create_array(&[Some("POINT (2 3)"), Some("POINT (2 3)")], &WKB_GEOMETRY);
+        let expected = &create_array(&[None, Some("POINT (0 0)")], &WKB_GEOMETRY);
+        let result = tester
+            .invoke_array_array(Arc::new(rasters), points)
+            .unwrap();
+        assert_array_equal(&result, expected);
+    }
+
     #[rstest]
     fn udf_invoke_pt() {
         let udf = rs_worldtorastercoord_udf();