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/30 17:01:42 UTC

(sedona) branch master updated: [GH-3403] Make ST_GeometryN, ST_InteriorRingN, RS_Value and RS_Values 1-based to match PostGIS (#3404)

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

james-willis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sedona.git


The following commit(s) were added to refs/heads/master by this push:
     new bb69775b3f1 [GH-3403] Make ST_GeometryN, ST_InteriorRingN, RS_Value and RS_Values 1-based to match PostGIS (#3404)
bb69775b3f1 is described below

commit bb69775b3f159872b4836da3f610f52d6ee42e17
Author: James Willis <ja...@users.noreply.github.com>
AuthorDate: Wed Sep 30 10:01:36 2026 -0700

    [GH-3403] Make ST_GeometryN, ST_InteriorRingN, RS_Value and RS_Values 1-based to match PostGIS (#3404)
---
 R/tests/testthat/test-dbplyr-integration.R         |   4 +-
 .../java/org/apache/sedona/common/Functions.java   |  16 +++-
 .../sedona/common/raster/PixelFunctions.java       |   4 +-
 .../org/apache/sedona/common/FunctionsTest.java    |  37 ++++++++
 .../apache/sedona/common/raster/FunctionsTest.java |  25 +++--
 .../common/raster/RasterConstructorsTest.java      |   4 +-
 docs/api/flink/Geometry-Accessors/ST_GeometryN.md  |   7 +-
 .../flink/Geometry-Accessors/ST_InteriorRingN.md   |   7 +-
 docs/api/flink/Geometry-Functions.md               |   4 +-
 docs/api/flink/Overlay-Functions/ST_SharedPaths.md |   4 +-
 .../vector-data/Geometry-Accessors/ST_GeometryN.md |   7 +-
 .../Geometry-Accessors/ST_InteriorRingN.md         |   7 +-
 .../snowflake/vector-data/Geometry-Functions.md    |   4 +-
 .../Overlay-Functions/ST_SharedPaths.md            |   4 +-
 docs/api/sql/Geometry-Accessors/ST_GeometryN.md    |   7 +-
 .../api/sql/Geometry-Accessors/ST_InteriorRingN.md |   7 +-
 docs/api/sql/Geometry-Functions.md                 |   4 +-
 docs/api/sql/Overlay-Functions/ST_SharedPaths.md   |   4 +-
 docs/api/sql/Raster-Operators/RS_Value.md          |   7 +-
 docs/api/sql/Raster-Operators/RS_Values.md         |   5 +-
 docs/image/ST_GeometryN/ST_GeometryN.svg           |   2 +-
 docs/image/ST_InteriorRingN/ST_InteriorRingN.svg   |   2 +-
 docs/setup/migration-2.0.md                        | 101 +++++++++++++++++++++
 docs/setup/release-notes.md                        |   7 ++
 .../java/org/apache/sedona/flink/FunctionTest.java |   4 +-
 mkdocs.yml                                         |   1 +
 python/sedona/spark/geopandas/_coverage.py         |   4 +-
 python/sedona/spark/geopandas/geoseries.py         |  11 ++-
 python/sedona/spark/sql/st_functions.py            |  20 ++--
 python/tests/sql/test_dataframe_api.py             |  12 +--
 python/tests/sql/test_function.py                  |   4 +-
 .../streaming/spark/test_constructor_functions.py  |   4 +-
 .../sedona/snowflake/snowsql/TestFunctions.java    |   4 +-
 .../sedona/snowflake/snowsql/TestFunctionsV2.java  |   4 +-
 .../org/apache/sedona/sql/PreserveSRIDSuite.scala  |   8 +-
 .../apache/sedona/sql/dataFrameAPITestScala.scala  |   4 +-
 .../org/apache/sedona/sql/functionTestScala.scala  |  20 ++--
 .../org/apache/sedona/sql/netcdfMetadataTest.scala |  30 +++---
 .../ogc/GeometryTypesAndFunctionsTestScala.scala   |   8 +-
 .../org/apache/sedona/sql/rasteralgebraTest.scala  |   8 +-
 40 files changed, 305 insertions(+), 121 deletions(-)

diff --git a/R/tests/testthat/test-dbplyr-integration.R b/R/tests/testthat/test-dbplyr-integration.R
index bb16fc02a36..fd5ad0eafac 100644
--- a/R/tests/testthat/test-dbplyr-integration.R
+++ b/R/tests/testthat/test-dbplyr-integration.R
@@ -113,7 +113,7 @@ test_that("ST_SimplifyPreserveTopology() works as expected", {
 test_that("ST_GeometryN() works as expected", {
   sdf <- sdf_len(sc, 1) %>%
     dplyr::mutate(pts = ST_GeomFromText("MULTIPOINT((1 2), (3 4), (5 6), (8 9))")) %>%
-    dplyr::transmute(pt = ST_GeometryN(pts, 2))
+    dplyr::transmute(pt = ST_GeometryN(pts, 3))
   df <- sdf %>% collect()
 
   expect_equal(nrow(df), 1)
@@ -130,7 +130,7 @@ test_that("ST_GeometryN() works as expected", {
 test_that("ST_InteriorRingN() works as expected", {
   sdf <- sdf_len(sc, 1) %>%
     dplyr::mutate(polygon = ST_GeomFromText("POLYGON((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1), (1 3, 2 3, 2 4, 1 4, 1 3), (3 3, 4 3, 4 4, 3 4, 3 3))")) %>%
-    dplyr::transmute(interior_ring = ST_InteriorRingN(polygon, 0))
+    dplyr::transmute(interior_ring = ST_InteriorRingN(polygon, 1))
   df <- sdf %>% collect()
 
   expect_equal(nrow(df), 1)
diff --git a/common/src/main/java/org/apache/sedona/common/Functions.java b/common/src/main/java/org/apache/sedona/common/Functions.java
index 030ec563bde..7067297739b 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -869,9 +869,13 @@ public class Functions {
     return geometry.reverse();
   }
 
+  /**
+   * Returns the 1-based nth geometry of a collection (PostGIS semantics). A non-collection is its
+   * own first geometry; an index outside 1..N returns null.
+   */
   public static Geometry geometryN(Geometry geometry, int n) {
-    if (n < geometry.getNumGeometries()) {
-      Geometry subGeom = geometry.getGeometryN(n);
+    if (n >= 1 && n <= geometry.getNumGeometries()) {
+      Geometry subGeom = geometry.getGeometryN(n - 1);
       if (subGeom.getSRID() == geometry.getSRID()) {
         return subGeom;
       }
@@ -880,11 +884,15 @@ public class Functions {
     return null;
   }
 
+  /**
+   * Returns the 1-based nth interior ring of a polygon (PostGIS semantics). An index outside 1..N
+   * returns null.
+   */
   public static Geometry interiorRingN(Geometry geometry, int n) {
     if (geometry instanceof Polygon) {
       Polygon polygon = (Polygon) geometry;
-      if (n < polygon.getNumInteriorRing()) {
-        Geometry interiorRing = polygon.getInteriorRingN(n);
+      if (n >= 1 && n <= polygon.getNumInteriorRing()) {
+        Geometry interiorRing = polygon.getInteriorRingN(n - 1);
         if (interiorRing instanceof LinearRing) {
           interiorRing = polygon.getFactory().createLineString(interiorRing.getCoordinates());
         }
diff --git a/common/src/main/java/org/apache/sedona/common/raster/PixelFunctions.java b/common/src/main/java/org/apache/sedona/common/raster/PixelFunctions.java
index 148431f009f..a8962daefec 100644
--- a/common/src/main/java/org/apache/sedona/common/raster/PixelFunctions.java
+++ b/common/src/main/java/org/apache/sedona/common/raster/PixelFunctions.java
@@ -240,7 +240,9 @@ public class PixelFunctions {
       int x = xCoordinates[i];
       int y = yCoordinates[i];
 
-      GridCoordinates2D gridCoord = new GridCoordinates2D(x, y);
+      // Pixel coordinates are 1-based (PostGIS semantics), like RS_PixelAsPoint; GeoTools
+      // grid space is 0-based.
+      GridCoordinates2D gridCoord = new GridCoordinates2D(x - 1, y - 1);
 
       try {
         pixelBuffer = rasterGeom.evaluate(gridCoord, pixelBuffer);
diff --git a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
index 3df0d1aacc1..de1a043b78f 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -3481,6 +3481,43 @@ public class FunctionsTest extends TestBase {
     assertEquals(second, second2);
   }
 
+  @Test
+  public void geometryNIsOneBased() throws ParseException {
+    // PostGIS semantics: n = 1 is the first element; 0, negatives and n > N are null.
+    Geometry multiPoint = geomFromWKT("MULTIPOINT ((1 1), (2 2), (3 3))", 4326);
+    assertEquals("POINT (1 1)", Functions.asWKT(Functions.geometryN(multiPoint, 1)));
+    assertEquals("POINT (3 3)", Functions.asWKT(Functions.geometryN(multiPoint, 3)));
+    assertEquals(4326, Functions.geometryN(multiPoint, 1).getSRID());
+    assertNull(Functions.geometryN(multiPoint, 0));
+    assertNull(Functions.geometryN(multiPoint, -1));
+    assertNull(Functions.geometryN(multiPoint, 4));
+
+    // A non-collection is its own first geometry.
+    Geometry point = geomFromWKT("POINT (1 1)", 0);
+    assertEquals("POINT (1 1)", Functions.asWKT(Functions.geometryN(point, 1)));
+    assertNull(Functions.geometryN(point, 0));
+    assertNull(Functions.geometryN(point, 2));
+  }
+
+  @Test
+  public void interiorRingNIsOneBased() throws ParseException {
+    // PostGIS semantics: n = 1 is the first hole; 0, negatives and n > N are null.
+    Geometry polygon =
+        geomFromWKT(
+            "POLYGON ((0 0, 6 0, 6 6, 0 6, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1), (4 4, 4 5, 5 5, 5 4, 4 4))",
+            0);
+    assertEquals(
+        "LINESTRING (1 1, 1 2, 2 2, 2 1, 1 1)",
+        Functions.asWKT(Functions.interiorRingN(polygon, 1)));
+    assertEquals(
+        "LINESTRING (4 4, 4 5, 5 5, 5 4, 4 4)",
+        Functions.asWKT(Functions.interiorRingN(polygon, 2)));
+    assertNull(Functions.interiorRingN(polygon, 0));
+    assertNull(Functions.interiorRingN(polygon, -1));
+    assertNull(Functions.interiorRingN(polygon, 3));
+    assertNull(Functions.interiorRingN(geomFromWKT("POINT (0 0)", 0), 1));
+  }
+
   @Test
   public void nRingsPolygonOnlyExternal() throws Exception {
     Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(1, 0, 1, 1, 2, 1, 2, 0, 1, 0));
diff --git a/common/src/test/java/org/apache/sedona/common/raster/FunctionsTest.java b/common/src/test/java/org/apache/sedona/common/raster/FunctionsTest.java
index dca3f149487..765547e9f51 100644
--- a/common/src/test/java/org/apache/sedona/common/raster/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/raster/FunctionsTest.java
@@ -99,10 +99,11 @@ public class FunctionsTest extends RasterTestBase {
 
   @Test
   public void valueWithGridCoords() throws TransformException {
-    int insideX = 1;
-    int insideY = 0;
-    int outsideX = 4;
-    int outsideY = 4;
+    // Grid coordinates are 1-based, as in PostGIS ST_Value.
+    int insideX = 2;
+    int insideY = 1;
+    int outsideX = 5;
+    int outsideY = 5;
 
     Double insideValue = PixelFunctions.value(oneBandRaster, insideX, insideY, 1);
     assertNotNull("Value should not be null for points inside the envelope.", insideValue);
@@ -110,12 +111,16 @@ public class FunctionsTest extends RasterTestBase {
         "Points outside of the envelope should return null.",
         PixelFunctions.value(oneBandRaster, outsideX, outsideY, 1));
 
-    int noDataX = 0;
-    int noDataY = 0;
+    int noDataX = 1;
+    int noDataY = 1;
 
     assertNull(
         "Null should be returned for no data values.",
         PixelFunctions.value(oneBandRaster, noDataX, noDataY, 1));
+
+    assertNull(
+        "Column 0 is outside the 1-based grid.", PixelFunctions.value(oneBandRaster, 0, 2, 1));
+    assertNull("Row 0 is outside the 1-based grid.", PixelFunctions.value(oneBandRaster, 2, 0, 1));
   }
 
   @Test
@@ -123,8 +128,8 @@ public class FunctionsTest extends RasterTestBase {
     // Multiband raster
     assertEquals(9d, PixelFunctions.value(multiBandRaster, point(4.5d, 4.5d), 3), 0.1d);
     assertEquals(255d, PixelFunctions.value(multiBandRaster, point(4.5d, 4.5d), 4), 0.1d);
-    assertEquals(4d, PixelFunctions.value(multiBandRaster, 2, 2, 3), 0.1d);
-    assertEquals(255d, PixelFunctions.value(multiBandRaster, 3, 4, 4), 0.1d);
+    assertEquals(4d, PixelFunctions.value(multiBandRaster, 3, 3, 3), 0.1d);
+    assertEquals(255d, PixelFunctions.value(multiBandRaster, 4, 5, 4), 0.1d);
   }
 
   @Test
@@ -394,8 +399,8 @@ public class FunctionsTest extends RasterTestBase {
 
   @Test
   public void valuesWithGridCoords() throws TransformException {
-    int[] xCoordinates = {1, 0};
-    int[] yCoordinates = {0, 1};
+    int[] xCoordinates = {2, 1};
+    int[] yCoordinates = {1, 2};
 
     List<Double> values = PixelFunctions.values(oneBandRaster, xCoordinates, yCoordinates, 1);
     assertEquals(2, values.size());
diff --git a/common/src/test/java/org/apache/sedona/common/raster/RasterConstructorsTest.java b/common/src/test/java/org/apache/sedona/common/raster/RasterConstructorsTest.java
index 0c3138950d7..b281d22193f 100644
--- a/common/src/test/java/org/apache/sedona/common/raster/RasterConstructorsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/raster/RasterConstructorsTest.java
@@ -1520,7 +1520,7 @@ public class RasterConstructorsTest extends RasterTestBase {
       assertEquals(expectedMetadata[i], actualMetadata[i], 1e-5);
     }
 
-    double actualFirstGridVal = PixelFunctions.value(testRaster, 0, 0, 1);
+    double actualFirstGridVal = PixelFunctions.value(testRaster, 1, 1, 1);
     double expectedFirstGridVal = 60.95357131958008;
     assertEquals(expectedFirstGridVal, actualFirstGridVal, 1e-6);
   }
@@ -1534,7 +1534,7 @@ public class RasterConstructorsTest extends RasterTestBase {
       assertEquals(expectedMetadata[i], actualMetadata[i], 1e-5);
     }
 
-    double actualFirstGridVal = PixelFunctions.value(testRaster, 0, 0, 1);
+    double actualFirstGridVal = PixelFunctions.value(testRaster, 1, 1, 1);
     double expectedFirstGridVal = 60.95357131958008;
     assertEquals(expectedFirstGridVal, actualFirstGridVal, 1e-6);
   }
diff --git a/docs/api/flink/Geometry-Accessors/ST_GeometryN.md b/docs/api/flink/Geometry-Accessors/ST_GeometryN.md
index c1b0afd79b6..d861120115f 100644
--- a/docs/api/flink/Geometry-Accessors/ST_GeometryN.md
+++ b/docs/api/flink/Geometry-Accessors/ST_GeometryN.md
@@ -19,7 +19,10 @@
 
 # ST_GeometryN
 
-Introduction: Return the 0-based Nth geometry if the geometry is a GEOMETRYCOLLECTION, (MULTI)POINT, (MULTI)LINESTRING, MULTICURVE or (MULTI)POLYGON. Otherwise, return null
+Introduction: Return the 1-based Nth geometry if the geometry is a GEOMETRYCOLLECTION, (MULTI)POINT, (MULTI)LINESTRING, MULTICURVE or (MULTI)POLYGON. Otherwise, return null
+
+!!!Note
+    Since `v2.0.0`, `n` is 1-based, as in PostGIS: `n = 1` returns the first geometry, and `0` or a negative `n` returns null. Earlier versions read `n` 0-based.
 
 ![ST_GeometryN](../../../image/ST_GeometryN/ST_GeometryN.svg "ST_GeometryN")
 
@@ -32,7 +35,7 @@ Since: `v1.3.0`
 Example:
 
 ```sql
-SELECT ST_GeometryN(ST_GeomFromText('MULTIPOINT((1 2), (3 4), (5 6), (8 9))'), 1)
+SELECT ST_GeometryN(ST_GeomFromText('MULTIPOINT((1 2), (3 4), (5 6), (8 9))'), 2)
 ```
 
 Output:
diff --git a/docs/api/flink/Geometry-Accessors/ST_InteriorRingN.md b/docs/api/flink/Geometry-Accessors/ST_InteriorRingN.md
index 3a08e5b38cd..ca1b23fe62c 100644
--- a/docs/api/flink/Geometry-Accessors/ST_InteriorRingN.md
+++ b/docs/api/flink/Geometry-Accessors/ST_InteriorRingN.md
@@ -19,7 +19,10 @@
 
 # ST_InteriorRingN
 
-Introduction: Returns the Nth interior linestring ring of the polygon geometry. Returns NULL if the geometry is not a polygon or the given N is out of range
+Introduction: Returns the Nth (1-based) interior linestring ring of the polygon geometry. Returns NULL if the geometry is not a polygon or the given N is out of range
+
+!!!Note
+    Since `v2.0.0`, `n` is 1-based, as in PostGIS: `n = 1` returns the first interior ring, and `0` or a negative `n` returns null. Earlier versions read `n` 0-based.
 
 ![ST_InteriorRingN](../../../image/ST_InteriorRingN/ST_InteriorRingN.svg "ST_InteriorRingN")
 
@@ -32,7 +35,7 @@ Since: `v1.3.0`
 Example:
 
 ```sql
-SELECT ST_InteriorRingN(ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1), (1 3, 2 3, 2 4, 1 4, 1 3), (3 3, 4 3, 4 4, 3 4, 3 3))'), 0)
+SELECT ST_InteriorRingN(ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1), (1 3, 2 3, 2 4, 1 4, 1 3), (3 3, 4 3, 4 4, 3 4, 3 3))'), 1)
 ```
 
 Output:
diff --git a/docs/api/flink/Geometry-Functions.md b/docs/api/flink/Geometry-Functions.md
index 0cc7fbab1f2..f00ee5c171d 100644
--- a/docs/api/flink/Geometry-Functions.md
+++ b/docs/api/flink/Geometry-Functions.md
@@ -73,11 +73,11 @@ These functions extract information and properties from geometry objects.
 | [ST_DumpPoints](Geometry-Accessors/ST_DumpPoints.md) | `Array<Geometry>` | Returns list of Points which geometry consists of. | v1.5.0 |
 | [ST_EndPoint](Geometry-Accessors/ST_EndPoint.md) | Geometry | Returns last point of given linestring. | v1.5.0 |
 | [ST_ExteriorRing](Geometry-Accessors/ST_ExteriorRing.md) | Geometry | Returns a LINESTRING representing the exterior ring (shell) of a POLYGON. Returns NULL if the geometry is not a polygon. | v1.2.1 |
-| [ST_GeometryN](Geometry-Accessors/ST_GeometryN.md) | Geometry | Return the 0-based Nth geometry if the geometry is a GEOMETRYCOLLECTION, (MULTI)POINT, (MULTI)LINESTRING, MULTICURVE or (MULTI)POLYGON. Otherwise, return null | v1.3.0 |
+| [ST_GeometryN](Geometry-Accessors/ST_GeometryN.md) | Geometry | Return the 1-based Nth geometry if the geometry is a GEOMETRYCOLLECTION, (MULTI)POINT, (MULTI)LINESTRING, MULTICURVE or (MULTI)POLYGON. Otherwise, return null | v1.3.0 |
 | [ST_GeometryType](Geometry-Accessors/ST_GeometryType.md) | String | Returns the type of the geometry as a string. EG: 'ST_Linestring', 'ST_Polygon' etc. | v1.5.0 |
 | [ST_HasM](Geometry-Accessors/ST_HasM.md) | Boolean | Checks for the presence of M coordinate values representing measures or linear references. Returns true if the input geometry includes an M coordinate, false otherwise. | v1.6.1 |
 | [ST_HasZ](Geometry-Accessors/ST_HasZ.md) | Boolean | Checks for the presence of Z coordinate values representing measures or linear references. Returns true if the input geometry includes an Z coordinate, false otherwise. | v1.6.1 |
-| [ST_InteriorRingN](Geometry-Accessors/ST_InteriorRingN.md) | Geometry | Returns the Nth interior linestring ring of the polygon geometry. Returns NULL if the geometry is not a polygon or the given N is out of range | v1.3.0 |
+| [ST_InteriorRingN](Geometry-Accessors/ST_InteriorRingN.md) | Geometry | Returns the Nth (1-based) interior linestring ring of the polygon geometry. Returns NULL if the geometry is not a polygon or the given N is out of range | v1.3.0 |
 | [ST_IsClosed](Geometry-Accessors/ST_IsClosed.md) | Boolean | RETURNS true if the LINESTRING start and end point are the same. | v1.3.0 |
 | [ST_IsCollection](Geometry-Accessors/ST_IsCollection.md) | Boolean | Returns `TRUE` if the geometry type of the input is a geometry collection type. Collection types are the following: | v1.5.0 |
 | [ST_IsEmpty](Geometry-Accessors/ST_IsEmpty.md) | Boolean | Test if a geometry is empty geometry | v1.2.1 |
diff --git a/docs/api/flink/Overlay-Functions/ST_SharedPaths.md b/docs/api/flink/Overlay-Functions/ST_SharedPaths.md
index cc4382c7647..fe2bcda975f 100644
--- a/docs/api/flink/Overlay-Functions/ST_SharedPaths.md
+++ b/docs/api/flink/Overlay-Functions/ST_SharedPaths.md
@@ -186,8 +186,8 @@ WITH compared AS (
 )
 SELECT
     segment_id,
-    ST_Length(ST_GeometryN(shared, 0)) AS same_direction_length,
-    ST_Length(ST_GeometryN(shared, 1)) AS opposite_direction_length
+    ST_Length(ST_GeometryN(shared, 1)) AS same_direction_length,
+    ST_Length(ST_GeometryN(shared, 2)) AS opposite_direction_length
 FROM compared;
 ```
 
diff --git a/docs/api/snowflake/vector-data/Geometry-Accessors/ST_GeometryN.md b/docs/api/snowflake/vector-data/Geometry-Accessors/ST_GeometryN.md
index 640a0345676..841215c7916 100644
--- a/docs/api/snowflake/vector-data/Geometry-Accessors/ST_GeometryN.md
+++ b/docs/api/snowflake/vector-data/Geometry-Accessors/ST_GeometryN.md
@@ -19,7 +19,10 @@
 
 # ST_GeometryN
 
-Introduction: Return the 0-based Nth geometry if the geometry is a GEOMETRYCOLLECTION, (MULTI)POINT, (MULTI)LINESTRING, MULTICURVE or (MULTI)POLYGON. Otherwise, return null
+Introduction: Return the 1-based Nth geometry if the geometry is a GEOMETRYCOLLECTION, (MULTI)POINT, (MULTI)LINESTRING, MULTICURVE or (MULTI)POLYGON. Otherwise, return null
+
+!!!Note
+    Since `v2.0.0`, `n` is 1-based, as in PostGIS: `n = 1` returns the first geometry, and `0` or a negative `n` returns null. Earlier versions read `n` 0-based.
 
 ![ST_GeometryN](../../../../image/ST_GeometryN/ST_GeometryN.svg "ST_GeometryN")
 
@@ -30,7 +33,7 @@ Return type: `Geometry`
 SQL example:
 
 ```sql
-SELECT ST_GeometryN(ST_GeomFromText('MULTIPOINT((1 2), (3 4), (5 6), (8 9))'), 1)
+SELECT ST_GeometryN(ST_GeomFromText('MULTIPOINT((1 2), (3 4), (5 6), (8 9))'), 2)
 ```
 
 Output: `POINT (3 4)`
diff --git a/docs/api/snowflake/vector-data/Geometry-Accessors/ST_InteriorRingN.md b/docs/api/snowflake/vector-data/Geometry-Accessors/ST_InteriorRingN.md
index ef79ca382d3..26bfa738256 100644
--- a/docs/api/snowflake/vector-data/Geometry-Accessors/ST_InteriorRingN.md
+++ b/docs/api/snowflake/vector-data/Geometry-Accessors/ST_InteriorRingN.md
@@ -19,7 +19,10 @@
 
 # ST_InteriorRingN
 
-Introduction: Returns the Nth interior linestring ring of the polygon geometry. Returns NULL if the geometry is not a polygon or the given N is out of range
+Introduction: Returns the Nth (1-based) interior linestring ring of the polygon geometry. Returns NULL if the geometry is not a polygon or the given N is out of range
+
+!!!Note
+    Since `v2.0.0`, `n` is 1-based, as in PostGIS: `n = 1` returns the first interior ring, and `0` or a negative `n` returns null. Earlier versions read `n` 0-based.
 
 ![ST_InteriorRingN](../../../../image/ST_InteriorRingN/ST_InteriorRingN.svg "ST_InteriorRingN")
 
@@ -30,7 +33,7 @@ Return type: `Geometry`
 SQL example:
 
 ```sql
-SELECT ST_InteriorRingN(ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1), (1 3, 2 3, 2 4, 1 4, 1 3), (3 3, 4 3, 4 4, 3 4, 3 3))'), 0)
+SELECT ST_InteriorRingN(ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1), (1 3, 2 3, 2 4, 1 4, 1 3), (3 3, 4 3, 4 4, 3 4, 3 3))'), 1)
 ```
 
 Output: `LINESTRING (1 1, 2 1, 2 2, 1 2, 1 1)`
diff --git a/docs/api/snowflake/vector-data/Geometry-Functions.md b/docs/api/snowflake/vector-data/Geometry-Functions.md
index 9b230673ae1..ba1729da434 100644
--- a/docs/api/snowflake/vector-data/Geometry-Functions.md
+++ b/docs/api/snowflake/vector-data/Geometry-Functions.md
@@ -71,10 +71,10 @@ These functions extract information and properties from geometry objects.
 | [ST_DumpPoints](Geometry-Accessors/ST_DumpPoints.md) | Returns a MultiPoint geometry which consists of individual points that compose the input line string. |
 | [ST_EndPoint](Geometry-Accessors/ST_EndPoint.md) | Returns last point of given linestring. |
 | [ST_ExteriorRing](Geometry-Accessors/ST_ExteriorRing.md) | Returns a line string representing the exterior ring of the POLYGON geometry. Return NULL if the geometry is not a polygon. |
-| [ST_GeometryN](Geometry-Accessors/ST_GeometryN.md) | Return the 0-based Nth geometry if the geometry is a GEOMETRYCOLLECTION, (MULTI)POINT, (MULTI)LINESTRING, MULTICURVE or (MULTI)POLYGON. Otherwise, return null |
+| [ST_GeometryN](Geometry-Accessors/ST_GeometryN.md) | Return the 1-based Nth geometry if the geometry is a GEOMETRYCOLLECTION, (MULTI)POINT, (MULTI)LINESTRING, MULTICURVE or (MULTI)POLYGON. Otherwise, return null |
 | [ST_GeometryType](Geometry-Accessors/ST_GeometryType.md) | Returns the type of the geometry as a string. EG: 'ST_Linestring', 'ST_Polygon' etc. |
 | [ST_HasZ](Geometry-Accessors/ST_HasZ.md) | Checks for the presence of Z coordinate values representing measures or linear references. Returns true if the input geometry includes an Z coordinate, false otherwise. |
-| [ST_InteriorRingN](Geometry-Accessors/ST_InteriorRingN.md) | Returns the Nth interior linestring ring of the polygon geometry. Returns NULL if the geometry is not a polygon or the given N is out of range |
+| [ST_InteriorRingN](Geometry-Accessors/ST_InteriorRingN.md) | Returns the Nth (1-based) interior linestring ring of the polygon geometry. Returns NULL if the geometry is not a polygon or the given N is out of range |
 | [ST_IsClosed](Geometry-Accessors/ST_IsClosed.md) | RETURNS true if the LINESTRING start and end point are the same. |
 | [ST_IsCollection](Geometry-Accessors/ST_IsCollection.md) | Returns `TRUE` if the geometry type of the input is a geometry collection type. Collection types are the following: |
 | [ST_IsEmpty](Geometry-Accessors/ST_IsEmpty.md) | Test if a geometry is empty geometry |
diff --git a/docs/api/snowflake/vector-data/Overlay-Functions/ST_SharedPaths.md b/docs/api/snowflake/vector-data/Overlay-Functions/ST_SharedPaths.md
index ee42132609b..9a67bc3bb7f 100644
--- a/docs/api/snowflake/vector-data/Overlay-Functions/ST_SharedPaths.md
+++ b/docs/api/snowflake/vector-data/Overlay-Functions/ST_SharedPaths.md
@@ -187,8 +187,8 @@ WITH compared AS (
 )
 SELECT
     segment_id,
-    ST_Length(ST_GeometryN(shared, 0)) AS same_direction_length,
-    ST_Length(ST_GeometryN(shared, 1)) AS opposite_direction_length
+    ST_Length(ST_GeometryN(shared, 1)) AS same_direction_length,
+    ST_Length(ST_GeometryN(shared, 2)) AS opposite_direction_length
 FROM compared;
 ```
 
diff --git a/docs/api/sql/Geometry-Accessors/ST_GeometryN.md b/docs/api/sql/Geometry-Accessors/ST_GeometryN.md
index bb4bb784fb3..b3c9566ac06 100644
--- a/docs/api/sql/Geometry-Accessors/ST_GeometryN.md
+++ b/docs/api/sql/Geometry-Accessors/ST_GeometryN.md
@@ -19,7 +19,10 @@
 
 # ST_GeometryN
 
-Introduction: Return the 0-based Nth geometry if the geometry is a GEOMETRYCOLLECTION, (MULTI)POINT, (MULTI)LINESTRING, MULTICURVE or (MULTI)POLYGON. Otherwise, return null
+Introduction: Return the 1-based Nth geometry if the geometry is a GEOMETRYCOLLECTION, (MULTI)POINT, (MULTI)LINESTRING, MULTICURVE or (MULTI)POLYGON. Otherwise, return null
+
+!!!Note
+    Since `v2.0.0`, `n` is 1-based, as in PostGIS: `n = 1` returns the first geometry, and `0` or a negative `n` returns null. Earlier versions read `n` 0-based.
 
 ![ST_GeometryN](../../../image/ST_GeometryN/ST_GeometryN.svg "ST_GeometryN")
 
@@ -32,7 +35,7 @@ Since: `v1.0.0`
 SQL Example
 
 ```sql
-SELECT ST_GeometryN(ST_GeomFromText('MULTIPOINT((1 2), (3 4), (5 6), (8 9))'), 1)
+SELECT ST_GeometryN(ST_GeomFromText('MULTIPOINT((1 2), (3 4), (5 6), (8 9))'), 2)
 ```
 
 Output:
diff --git a/docs/api/sql/Geometry-Accessors/ST_InteriorRingN.md b/docs/api/sql/Geometry-Accessors/ST_InteriorRingN.md
index 09595fa89e5..c3958cb6549 100644
--- a/docs/api/sql/Geometry-Accessors/ST_InteriorRingN.md
+++ b/docs/api/sql/Geometry-Accessors/ST_InteriorRingN.md
@@ -19,7 +19,10 @@
 
 # ST_InteriorRingN
 
-Introduction: Returns the Nth interior linestring ring of the polygon geometry. Returns NULL if the geometry is not a polygon or the given N is out of range
+Introduction: Returns the Nth (1-based) interior linestring ring of the polygon geometry. Returns NULL if the geometry is not a polygon or the given N is out of range
+
+!!!Note
+    Since `v2.0.0`, `n` is 1-based, as in PostGIS: `n = 1` returns the first interior ring, and `0` or a negative `n` returns null. Earlier versions read `n` 0-based.
 
 ![ST_InteriorRingN](../../../image/ST_InteriorRingN/ST_InteriorRingN.svg "ST_InteriorRingN")
 
@@ -32,7 +35,7 @@ Since: `v1.0.0`
 SQL Example
 
 ```sql
-SELECT ST_InteriorRingN(ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1), (1 3, 2 3, 2 4, 1 4, 1 3), (3 3, 4 3, 4 4, 3 4, 3 3))'), 0)
+SELECT ST_InteriorRingN(ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1), (1 3, 2 3, 2 4, 1 4, 1 3), (3 3, 4 3, 4 4, 3 4, 3 3))'), 1)
 ```
 
 Output:
diff --git a/docs/api/sql/Geometry-Functions.md b/docs/api/sql/Geometry-Functions.md
index 4ba892fa51a..6c286d6fbb3 100644
--- a/docs/api/sql/Geometry-Functions.md
+++ b/docs/api/sql/Geometry-Functions.md
@@ -74,11 +74,11 @@ These functions extract information and properties from geometry objects.
 | [ST_DumpPoints](Geometry-Accessors/ST_DumpPoints.md) | `Array<Geometry>` | Returns list of Points which geometry consists of. | v1.0.0 |
 | [ST_EndPoint](Geometry-Accessors/ST_EndPoint.md) | Geometry | Returns last point of given linestring. | v1.0.0 |
 | [ST_ExteriorRing](Geometry-Accessors/ST_ExteriorRing.md) | Geometry | Returns a line string representing the exterior ring of the POLYGON geometry. Return NULL if the geometry is not a polygon. | v1.0.0 |
-| [ST_GeometryN](Geometry-Accessors/ST_GeometryN.md) | Geometry | Return the 0-based Nth geometry if the geometry is a GEOMETRYCOLLECTION, (MULTI)POINT, (MULTI)LINESTRING, MULTICURVE or (MULTI)POLYGON. Otherwise, return null | v1.0.0 |
+| [ST_GeometryN](Geometry-Accessors/ST_GeometryN.md) | Geometry | Return the 1-based Nth geometry if the geometry is a GEOMETRYCOLLECTION, (MULTI)POINT, (MULTI)LINESTRING, MULTICURVE or (MULTI)POLYGON. Otherwise, return null | v1.0.0 |
 | [ST_GeometryType](Geometry-Accessors/ST_GeometryType.md) | String | Returns the type of the geometry as a string. EG: 'ST_Linestring', 'ST_Polygon' etc. | v1.0.0 |
 | [ST_HasM](Geometry-Accessors/ST_HasM.md) | Boolean | Checks for the presence of M coordinate values representing measures or linear references. Returns true if the input geometry includes an M coordinate, false otherwise. | v1.6.1 |
 | [ST_HasZ](Geometry-Accessors/ST_HasZ.md) | Boolean | Checks for the presence of Z coordinate values representing measures or linear references. Returns true if the input geometry includes an Z coordinate, false otherwise. | v1.6.1 |
-| [ST_InteriorRingN](Geometry-Accessors/ST_InteriorRingN.md) | Geometry | Returns the Nth interior linestring ring of the polygon geometry. Returns NULL if the geometry is not a polygon or the given N is out of range | v1.0.0 |
+| [ST_InteriorRingN](Geometry-Accessors/ST_InteriorRingN.md) | Geometry | Returns the Nth (1-based) interior linestring ring of the polygon geometry. Returns NULL if the geometry is not a polygon or the given N is out of range | v1.0.0 |
 | [ST_IsClosed](Geometry-Accessors/ST_IsClosed.md) | Boolean | RETURNS true if the LINESTRING start and end point are the same. | v1.0.0 |
 | [ST_IsCollection](Geometry-Accessors/ST_IsCollection.md) | Boolean | Returns `TRUE` if the geometry type of the input is a geometry collection type. Collection types are the following: | v1.5.0 |
 | [ST_IsEmpty](Geometry-Accessors/ST_IsEmpty.md) | Boolean | Test if a geometry is empty geometry | v1.2.1 |
diff --git a/docs/api/sql/Overlay-Functions/ST_SharedPaths.md b/docs/api/sql/Overlay-Functions/ST_SharedPaths.md
index cc4382c7647..fe2bcda975f 100644
--- a/docs/api/sql/Overlay-Functions/ST_SharedPaths.md
+++ b/docs/api/sql/Overlay-Functions/ST_SharedPaths.md
@@ -186,8 +186,8 @@ WITH compared AS (
 )
 SELECT
     segment_id,
-    ST_Length(ST_GeometryN(shared, 0)) AS same_direction_length,
-    ST_Length(ST_GeometryN(shared, 1)) AS opposite_direction_length
+    ST_Length(ST_GeometryN(shared, 1)) AS same_direction_length,
+    ST_Length(ST_GeometryN(shared, 2)) AS opposite_direction_length
 FROM compared;
 ```
 
diff --git a/docs/api/sql/Raster-Operators/RS_Value.md b/docs/api/sql/Raster-Operators/RS_Value.md
index 34b3530f4ba..26c0d364b7b 100644
--- a/docs/api/sql/Raster-Operators/RS_Value.md
+++ b/docs/api/sql/Raster-Operators/RS_Value.md
@@ -24,13 +24,16 @@ Introduction: Returns the value at the given point in the raster. If no band num
 !!!Note
     Since `v1.5.1`, if the coordinate reference system (CRS) of the input `point` geometry differs from that of the `raster`, then `point` will be transformed to match the CRS of the `raster`. If the `raster` or `point` doesn't have a CRS then it will default to `4326/WGS84`.
 
+!!!Note
+    Since `v2.0.0`, grid coordinates are 1-based, as in PostGIS `ST_Value`: `(1, 1)` is the upper-left pixel. Earlier versions read them 0-based.
+
 Format:
 
 `RS_Value (raster: Raster, point: Geometry)`
 
 `RS_Value (raster: Raster, point: Geometry, band: Integer)`
 
-`RS_Value (raster: Raster, colX: Integer, colY: Integer, band: Integer)`
+`RS_Value (raster: Raster, colX: Integer, rowY: Integer, band: Integer)`
 
 Return type: `Double`
 
@@ -47,7 +50,7 @@ SELECT RS_Value(raster, ST_Point(-13077301.685, 4002565.802)) FROM raster_table
 - For Grid Coordinates:
 
 ```sql
-SELECT RS_Value(raster, 3, 4, 1) FROM raster_table
+SELECT RS_Value(raster, 4, 5, 1) FROM raster_table
 ```
 
 Output:
diff --git a/docs/api/sql/Raster-Operators/RS_Values.md b/docs/api/sql/Raster-Operators/RS_Values.md
index d971edfaeb4..ab2d90bdf85 100644
--- a/docs/api/sql/Raster-Operators/RS_Values.md
+++ b/docs/api/sql/Raster-Operators/RS_Values.md
@@ -27,6 +27,9 @@ RS_Values can be significantly faster since a raster only has to be loaded once
 !!!Note
     Since `v1.5.1`, if the coordinate reference system (CRS) of the input `points` geometries differs from that of the `raster`, then `points` will be transformed to match the CRS of the `raster`. If the `raster` or `points` doesn't have a CRS then it will default to `4326/WGS84`.
 
+!!!Note
+    Since `v2.0.0`, grid coordinates are 1-based, as in PostGIS `ST_Value`: `(1, 1)` is the upper-left pixel. Earlier versions read them 0-based.
+
 Format:
 
 `RS_Values (raster: Raster, points: ARRAY[Geometry])`
@@ -53,7 +56,7 @@ FROM raster_table
 - For Arrays of grid coordinates:
 
 ```sql
-SELECT RS_Values(raster, Array(4, 5), Array(3, 2), 1) FROM raster_table
+SELECT RS_Values(raster, Array(5, 6), Array(4, 3), 1) FROM raster_table
 ```
 
 Output:
diff --git a/docs/image/ST_GeometryN/ST_GeometryN.svg b/docs/image/ST_GeometryN/ST_GeometryN.svg
index a6795b30986..d7ff2330f60 100644
--- a/docs/image/ST_GeometryN/ST_GeometryN.svg
+++ b/docs/image/ST_GeometryN/ST_GeometryN.svg
@@ -10,6 +10,6 @@
   <circle cx="396.67" cy="260.0" r="6" fill="#4a90d9" fill-opacity="1.0" stroke="#4a90d9" stroke-width="1.5"/>
   <circle cx="250.0" cy="40.0" r="6" fill="#4a90d9" fill-opacity="1.0" stroke="#4a90d9" stroke-width="1.5"/>
   <circle cx="250.0" cy="40.0" r="7" fill="#2ecc71" fill-opacity="1.0" stroke="#2ecc71" stroke-width="1.5"/>
-  <text x="250.0" y="22" text-anchor="middle" fill="#333" font-size="13" font-weight="bold">N = 1</text>
+  <text x="250.0" y="22" text-anchor="middle" fill="#333" font-size="13" font-weight="bold">N = 2</text>
   <text x="490" y="290" text-anchor="end" fill="#999" font-size="11">ST_GeometryN</text>
 </svg>
\ No newline at end of file
diff --git a/docs/image/ST_InteriorRingN/ST_InteriorRingN.svg b/docs/image/ST_InteriorRingN/ST_InteriorRingN.svg
index 7a7edbc110f..9fed3ac59d0 100644
--- a/docs/image/ST_InteriorRingN/ST_InteriorRingN.svg
+++ b/docs/image/ST_InteriorRingN/ST_InteriorRingN.svg
@@ -9,6 +9,6 @@
   <polygon points="176.67,223.33 213.33,223.33 213.33,186.67 176.67,186.67 176.67,223.33" fill="#4a90d9" fill-opacity="0" stroke="#4a90d9" stroke-width="2.5" stroke-opacity="1.0" stroke-dasharray="6,3"/>
   <polygon points="250.0,150.0 323.33,150.0 323.33,76.67 250.0,76.67 250.0,150.0" fill="#4a90d9" fill-opacity="0" stroke="#4a90d9" stroke-width="2.5" stroke-opacity="1.0" stroke-dasharray="6,3"/>
   <polyline points="176.67,223.33 213.33,223.33 213.33,186.67 176.67,186.67 176.67,223.33" fill="none" stroke="#2ecc71" stroke-width="3" stroke-opacity="1.0"/>
-  <text x="250.0" y="22" text-anchor="middle" fill="#333" font-size="13" font-weight="bold">N = 0</text>
+  <text x="250.0" y="22" text-anchor="middle" fill="#333" font-size="13" font-weight="bold">N = 1</text>
   <text x="490" y="290" text-anchor="end" fill="#999" font-size="11">ST_InteriorRingN</text>
 </svg>
\ No newline at end of file
diff --git a/docs/setup/migration-2.0.md b/docs/setup/migration-2.0.md
new file mode 100644
index 00000000000..69b973abdbd
--- /dev/null
+++ b/docs/setup/migration-2.0.md
@@ -0,0 +1,101 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+# Migrating to Sedona 2.0
+
+Sedona 2.0 is a major release. Most code runs unchanged, but a few behaviours change on
+purpose: Sedona now matches PostGIS in places where it used to differ, and some inputs that
+were silently mishandled now raise an error instead. This page walks through what to check
+when you upgrade from 1.x. The [release notes](release-notes.md#sedona-200) list every change.
+
+## Check your platform
+
+* **Spark 3.5 or newer.** Spark 3.4 is no longer supported, and no `sedona-spark-3.4_*`
+  artifacts are published for 2.0. Supported versions are Spark 3.5, 4.0 and 4.1. The Python
+  package requires `pyspark>=3.5.0`.
+* **Flink 1.19 or newer.** Flink 1.12 to 1.18 are no longer supported. Flink 2.2 is supported.
+  Stateful Flink jobs that hold an `ST_MinimumBoundingRadius` result in their state can't
+  restore from a checkpoint or savepoint taken with 1.x. Restart them without state.
+
+## Element and pixel positions now count from 1
+
+Four functions used to count positions from 0 and now count from 1, as PostGIS does. This is
+the change most likely to affect existing queries, because the old queries still run: they
+now return the *next* element or pixel, or `NULL` past the end, rather than raising an error.
+
+| Function | Sedona 1.x | Sedona 2.0 (= PostGIS) |
+|---|---|---|
+| `ST_GeometryN(geom, n)` | `n = 0` is the first geometry | `n = 1` is the first geometry |
+| `ST_InteriorRingN(polygon, n)` | `n = 0` is the first hole | `n = 1` is the first hole |
+| `RS_Value(raster, colX, rowY, band)` | `(0, 0)` is the upper-left pixel | `(1, 1)` is the upper-left pixel |
+| `RS_Values(raster, xs, ys, band)` | `(0, 0)` is the upper-left pixel | `(1, 1)` is the upper-left pixel |
+
+In 2.0, an index of `0`, a negative index, or one past the last element returns `NULL`.
+
+**What to change:** add 1 to the index or grid coordinate in every call to these four
+functions, whether it's a literal or a column. For example:
+
+```sql
+-- Sedona 1.x
+SELECT ST_GeometryN(geom, 0), RS_Value(rast, 3, 4, 1) FROM t
+-- Sedona 2.0
+SELECT ST_GeometryN(geom, 1), RS_Value(rast, 4, 5, 1) FROM t
+```
+
+Code that builds the index with `sequence(0, n - 1)` or a `range(0, n)` loop needs
+`sequence(1, n)` or `range(1, n + 1)` instead.
+
+**What doesn't change:**
+
+* The point-geometry forms `RS_Value(raster, point[, band])` and `RS_Values(raster, points[, band])`.
+* `RS_PixelAsPoint`, `RS_PixelAsCentroid`, `RS_PixelAsPolygon`, `RS_SetValue`, `RS_SetValues`,
+  `RS_RasterToWorldCoord` and `RS_WorldToRasterCoord`, which were already 1-based, as were all
+  band arguments and `ST_PointN`.
+* `ST_AddPoint`, `ST_SetPoint` and `ST_RemovePoint`, which stay 0-based because PostGIS is
+  0-based for these.
+* The GeoPandas API: `GeoSeries.get_geometry` and `GeoSeries.interiors` keep GeoPandas'
+  0-based indexing and handle the change internally.
+
+The Python `ST_GeometryN` and `ST_InteriorRingN` DataFrame functions now reject an integer
+`n` below 1 rather than below 0.
+
+## Other behaviour changes
+
+* **WKB output keeps M.** `ST_AsBinary`, `ST_AsEWKB` and `ST_AsHEXEWKB` now write the M
+  ordinate of measured geometries, so these inputs produce XYM or XYZM rather than XY or XYZ.
+* **`ST_IsPolygonCW` and `ST_IsPolygonCCW`** look inside nested `GeometryCollection` values and
+  return `true` for inputs with no polygons, as PostGIS does. In Flink, a `NULL` input now returns
+  `NULL`.
+* **Mixed coordinate layouts are rejected.** Serializing a polygon or multi-geometry whose parts
+  mix XY, XYZ, XYM or XYZM (for example from `ST_Collect`) now raises an error instead of
+  corrupting ordinates. Normalize the parts to one layout, or use a `GeometryCollection`.
+* **`RS_Resample` and `RS_ReprojectMatch`** raise an error for an unknown algorithm name.
+  Previously they silently fell back to nearest neighbour.
+* **`RS_SetBandNoDataValue(raster, band, NULL)`** now removes that band's no-data value from
+  Spark SQL, as documented, and `RS_AsGeoTiff` no longer writes a no-data value of `0` for a
+  raster that has none.
+* **GeoPandas `set_crs`** defaults to `allow_override=False`, as GeoPandas does, so replacing an
+  existing CRS needs `allow_override=True`.
+* **GeoPandas `fillna`** validates a replacement Series' index when Spark evaluates the result,
+  so a bad index raises a Spark error instead of an immediate `ValueError`.
+
+## Deprecations
+
+* **`ST_Force_2D`** is deprecated in favour of `ST_Force2D`, the name PostGIS uses. The old name
+  still works in 2.0 but logs a warning.
diff --git a/docs/setup/release-notes.md b/docs/setup/release-notes.md
index 0b8821f5657..6e6b2e6c5ff 100644
--- a/docs/setup/release-notes.md
+++ b/docs/setup/release-notes.md
@@ -19,8 +19,15 @@
 
 ## Sedona 2.0.0
 
+See [Migrating to Sedona 2.0](migration-2.0.md) for what to check when upgrading from 1.x.
+
 ### Breaking Changes
 
+* **Positions count from 1, as in PostGIS**: `ST_GeometryN` and `ST_InteriorRingN` read `n`
+  1-based, and the grid-coordinate forms of `RS_Value` and `RS_Values` read `(1, 1)` as the
+  upper-left pixel. They were 0-based, so existing queries now address the next element or pixel;
+  add 1 to the index. An index of `0` or below returns `NULL`.
+
 * **WKB output**: `ST_AsBinary`, `ST_AsEWKB`, and `ST_AsHEXEWKB` now retain M for measured
   geometries in Spark, Flink, and Snowflake, including geometries read from WKT. Earlier
   versions dropped M, so these inputs now produce XYM or XYZM instead of XY or XYZ.
diff --git a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
index 059aebb1013..053d4a5a6c6 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -1139,7 +1139,7 @@ public class FunctionTest extends TestBase {
             "SELECT ST_GeomFromWKT('GEOMETRYCOLLECTION(POINT(10 10), POINT(30 30), LINESTRING(15 15, 20 20))') AS collection");
     Table resultTable =
         collectionTable.select(
-            call(Functions.ST_GeometryN.class.getSimpleName(), $("collection"), 1));
+            call(Functions.ST_GeometryN.class.getSimpleName(), $("collection"), 2));
     Point point = (Point) first(resultTable).getField(0);
     assertEquals("POINT (30 30)", point.toString());
   }
@@ -1151,7 +1151,7 @@ public class FunctionTest extends TestBase {
             "SELECT ST_GeomFromText('POLYGON((7 9,8 7,11 6,15 8,16 6,17 7,17 10,18 12,17 14,15 15,11 15,10 13,9 12,7 9),(9 9,10 10,11 11,11 10,10 8,9 9),(12 14,15 14,13 11,12 14))') AS polygon");
     Table resultTable =
         polygonTable.select(
-            call(Functions.ST_InteriorRingN.class.getSimpleName(), $("polygon"), 1));
+            call(Functions.ST_InteriorRingN.class.getSimpleName(), $("polygon"), 2));
     LineString lineString = (LineString) first(resultTable).getField(0);
     assertEquals("LINESTRING (12 14, 15 14, 13 11, 12 14)", lineString.toString());
   }
diff --git a/mkdocs.yml b/mkdocs.yml
index 26770aca13e..199599fa30c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -105,6 +105,7 @@ nav:
           - GeoPandas API coverage: api/geopandas-coverage.md
           - Sedona Python: api/pydocs
       - Release notes: setup/release-notes.md
+      - Migrating to Sedona 2.0: setup/migration-2.0.md
 
   - SedonaFlink:
       - Home: sedonaflink.md
diff --git a/python/sedona/spark/geopandas/_coverage.py b/python/sedona/spark/geopandas/_coverage.py
index 02007c03b38..f03b2bf3b5c 100644
--- a/python/sedona/spark/geopandas/_coverage.py
+++ b/python/sedona/spark/geopandas/_coverage.py
@@ -105,7 +105,7 @@ def _extract(frame: DataFrame) -> DataFrame:
             "_line",
             F.expr(
                 "CASE WHEN ring = 0 THEN ST_ExteriorRing(_polygon) "
-                "ELSE ST_InteriorRingN(_polygon, ring - 1) END"
+                "ELSE ST_InteriorRingN(_polygon, ring) END"
             ),
         )
         .withColumn("_points", F.expr("ST_DumpPoints(_line)"))
@@ -397,7 +397,7 @@ def simplify_coverage(
         if (
             original.where(
                 "exists(ST_Dump(geom), p -> CASE WHEN ST_NumInteriorRings(p) = 0 "
-                "THEN false ELSE exists(sequence(0, ST_NumInteriorRings(p) - 1), "
+                "THEN false ELSE exists(sequence(1, ST_NumInteriorRings(p)), "
                 "i -> ST_IsEmpty(ST_InteriorRingN(p, i))) END)"
             )
             .limit(1)
diff --git a/python/sedona/spark/geopandas/geoseries.py b/python/sedona/spark/geopandas/geoseries.py
index be1e609bd5f..9f308abbe0c 100644
--- a/python/sedona/spark/geopandas/geoseries.py
+++ b/python/sedona/spark/geopandas/geoseries.py
@@ -1919,15 +1919,16 @@ class GeoSeries(GeoFrame, pspd.Series):
         raise NotImplementedError("This method is not implemented yet.")
 
     def get_geometry(self, index) -> "GeoSeries":
-        # Sedona errors on negative indexes, so we use a case statement to handle it ourselves.
+        # GeoPandas indexes are 0-based and may be negative, while ST_GeometryN is
+        # 1-based, so translate here: -1 is the last geometry.
         spark_expr = stf.ST_GeometryN(
             F.col("L"),
             F.when(
                 stf.ST_NumGeometries(F.col("L")) + F.col("R") < 0,
                 None,
             )
-            .when(F.col("R") < 0, stf.ST_NumGeometries(F.col("L")) + F.col("R"))
-            .otherwise(F.col("R")),
+            .when(F.col("R") < 0, stf.ST_NumGeometries(F.col("L")) + F.col("R") + 1)
+            .otherwise(F.col("R") + 1),
         )
 
         other, _ = self._make_series_of_val(index)
@@ -2092,10 +2093,10 @@ class GeoSeries(GeoFrame, pspd.Series):
         ring_count = stf.ST_NumInteriorRings(geometry)
         empty_rings = F.slice(stf.ST_Dump(geometry), 1, 0)
         rings = F.transform(
-            F.sequence(F.lit(0), ring_count - 1),
+            F.sequence(F.lit(1), ring_count),
             lambda index: stf.ST_InteriorRingN(geometry, index),
         )
-        # Spark creates a descending sequence for sequence(0, -1), so this guard
+        # Spark creates a descending sequence for sequence(1, 0), so this guard
         # is required to keep polygons without interior rings as an empty array.
         polygon_rings = F.when(ring_count > 0, rings).otherwise(empty_rings)
         spark_expr = F.when(
diff --git a/python/sedona/spark/sql/st_functions.py b/python/sedona/spark/sql/st_functions.py
index 09968ef58a3..05800bc7fad 100644
--- a/python/sedona/spark/sql/st_functions.py
+++ b/python/sedona/spark/sql/st_functions.py
@@ -877,18 +877,18 @@ def ST_GeometricMedian(
 
 @validate_argument_types
 def ST_GeometryN(multi_geometry: ColumnOrName, n: Union[ColumnOrName, int]) -> Column:
-    """Return the geometry at index n (0-th based) of a multi-geometry column.
+    """Return the geometry at index n (1-based) of a multi-geometry column.
 
     :param multi_geometry: Multi-geometry column to get from.
     :type multi_geometry: ColumnOrName
-    :param n: Index to select, given as an integer or integer column, 0-th based index, returns null if index is greater than maximum index.
+    :param n: Index to select, given as an integer or integer column, 1-based index, returns null if index is greater than maximum index.
     :type n: Union[ColumnOrName, int]
     :return: Geometry located at index n in multi_geometry as a geometry column.
     :rtype: Column
-    :raises ValueError: If
+    :raises ValueError: If n is an integer and less than 1.
     """
-    if isinstance(n, int) and n < 0:
-        raise ValueError(f"Index n for ST_GeometryN must by >= 0: {n} < 0")
+    if isinstance(n, int) and n < 1:
+        raise ValueError(f"Index n for ST_GeometryN must be >= 1: {n} < 1")
     return _call_st_function("ST_GeometryN", (multi_geometry, n))
 
 
@@ -1155,18 +1155,18 @@ def ST_BingTileToGeom(quad_keys: Union[ColumnOrName, list]) -> Column:
 
 @validate_argument_types
 def ST_InteriorRingN(polygon: ColumnOrName, n: Union[ColumnOrName, int]) -> Column:
-    """Return the index n (0-th based) interior ring of a polygon geometry column.
+    """Return the index n (1-based) interior ring of a polygon geometry column.
 
     :param polygon: Polygon geometry column to get an interior ring from.
     :type polygon: ColumnOrName
-    :param n: Index of interior ring to return as either an integer or integer column, 0-th based.
+    :param n: Index of interior ring to return as either an integer or integer column, 1-based.
     :type n: Union[ColumnOrName, int]
-    :raises ValueError: If n is an integer and less than 0.
+    :raises ValueError: If n is an integer and less than 1.
     :return: Interior ring at index n as a linestring geometry column or null if n is greater than maximum index
     :rtype: Column
     """
-    if isinstance(n, int) and n < 0:
-        raise ValueError(f"Index n for ST_InteriorRingN must by >= 0: {n} < 0")
+    if isinstance(n, int) and n < 1:
+        raise ValueError(f"Index n for ST_InteriorRingN must be >= 1: {n} < 1")
     return _call_st_function("ST_InteriorRingN", (polygon, n))
 
 
diff --git a/python/tests/sql/test_dataframe_api.py b/python/tests/sql/test_dataframe_api.py
index 20a44c9b999..323dbedb92d 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -664,7 +664,7 @@ test_configurations = [
         "ST_NumGeometries(geom)",
         15,
     ),
-    (stf.ST_GeometryN, ("geom", 0), "multipoint", "", "POINT (0 0)"),
+    (stf.ST_GeometryN, ("geom", 1), "multipoint", "", "POINT (0 0)"),
     (stf.ST_GeometryType, ("point",), "point_geom", "", "ST_Point"),
     (
         stf.ST_HilbertDistance,
@@ -708,7 +708,7 @@ test_configurations = [
     ),
     (
         stf.ST_InteriorRingN,
-        ("geom", 0),
+        ("geom", 1),
         "geom_with_hole",
         "",
         "LINESTRING (1 1, 2 2, 2 1, 1 1)",
@@ -1537,9 +1537,9 @@ wrong_type_configurations = [
     (stf.ST_ForcePolygonCW, (None,)),
     (stf.ST_ForcePolygonCCW, (None,)),
     (stf.ST_ForceRHR, (None,)),
-    (stf.ST_GeometryN, (None, 0)),
+    (stf.ST_GeometryN, (None, 1)),
     (stf.ST_GeometryN, ("", None)),
-    (stf.ST_GeometryN, ("", 0.0)),
+    (stf.ST_GeometryN, ("", 1.0)),
     (stf.ST_GeometryType, (None,)),
     (stf.ST_HilbertDistance, (None, 0.0, 0.0, 1.0, 1.0, 2)),
     (stf.ST_HilbertDistance, ("", None, 0.0, 1.0, 1.0, 2)),
@@ -1549,9 +1549,9 @@ wrong_type_configurations = [
     (stf.ST_GeoHashNeighbor, ("", None)),
     (stf.ST_GeneratePoints, (None, 0.0)),
     (stf.ST_GeneratePoints, ("", None)),
-    (stf.ST_InteriorRingN, (None, 0)),
+    (stf.ST_InteriorRingN, (None, 1)),
     (stf.ST_InteriorRingN, ("", None)),
-    (stf.ST_InteriorRingN, ("", 0.0)),
+    (stf.ST_InteriorRingN, ("", 1.0)),
     (stf.ST_Intersection, (None, "")),
     (stf.ST_Intersection, ("", None)),
     (stf.ST_SharedPaths, (None, "")),
diff --git a/python/tests/sql/test_function.py b/python/tests/sql/test_function.py
index baa4d775fcd..d3657b38055 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -1104,7 +1104,7 @@ class TestPredicateJoin(TestBase):
             data_frame.selectExpr(f"ST_GeometryN(geom, {i}) as geom")
             .selectExpr("st_asText(geom)")
             .collect()[0][0]
-            for i in range(0, 4)
+            for i in range(1, 5)
         ]
 
         assert wkts == ["POINT (1 2)", "POINT (3 4)", "POINT (5 6)", "POINT (8 9)"]
@@ -1123,7 +1123,7 @@ class TestPredicateJoin(TestBase):
             polygon_df.selectExpr(f"ST_InteriorRingN(geom, {i}) as geom")
             .selectExpr("ST_AsText(geom)")
             .collect()[0][0]
-            for i in range(3)
+            for i in range(1, 4)
         ]
 
         empty_df = other_geometry.selectExpr(
diff --git a/python/tests/streaming/spark/test_constructor_functions.py b/python/tests/streaming/spark/test_constructor_functions.py
index 6cf84749f1c..943589a4dad 100644
--- a/python/tests/streaming/spark/test_constructor_functions.py
+++ b/python/tests/streaming/spark/test_constructor_functions.py
@@ -319,7 +319,7 @@ SEDONA_LISTED_SQL_FUNCTIONS = [
         SuiteContainer.empty()
         .with_function_name("ST_GeometryN")
         .with_arguments(
-            ["ST_GeomFromText('MULTIPOINT((1 2), (3 4), (5 6), (8 9))')", "0"]
+            ["ST_GeomFromText('MULTIPOINT((1 2), (3 4), (5 6), (8 9))')", "1"]
         )
         .with_expected_result(1)
         .with_transform("ST_X")
@@ -330,7 +330,7 @@ SEDONA_LISTED_SQL_FUNCTIONS = [
         .with_arguments(
             [
                 "ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1))')",
-                "0",
+                "1",
             ]
         )
         .with_expected_result(4.0)
diff --git a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
index 65de74c7967..140f58e6d86 100644
--- a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
+++ b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
@@ -488,7 +488,7 @@ public class TestFunctions extends TestBase {
   public void test_ST_GeometryN() {
     registerUDF("ST_GeometryN", byte[].class, int.class);
     verifySqlSingleRes(
-        "select sedona.ST_AsText(sedona.ST_GeometryN(sedona.ST_GeomFromText('MULTIPOINT((10 40), (40 30), (20 20), (30 10))'), 1))",
+        "select sedona.ST_AsText(sedona.ST_GeometryN(sedona.ST_GeomFromText('MULTIPOINT((10 40), (40 30), (20 20), (30 10))'), 2))",
         "POINT (40 30)");
   }
 
@@ -535,7 +535,7 @@ public class TestFunctions extends TestBase {
   public void test_ST_InteriorRingN() {
     registerUDF("ST_InteriorRingN", byte[].class, int.class);
     verifySqlSingleRes(
-        "select sedona.ST_AsText(sedona.ST_InteriorRingN(sedona.ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1), (1 3, 2 3, 2 4, 1 4, 1 3), (3 3, 4 3, 4 4, 3 4, 3 3))'), 0))",
+        "select sedona.ST_AsText(sedona.ST_InteriorRingN(sedona.ST_GeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1), (1 3, 2 3, 2 4, 1 4, 1 3), (3 3, 4 3, 4 4, 3 4, 3 3))'), 1))",
         "LINESTRING (1 1, 2 1, 2 2, 1 2, 1 1)");
   }
 
diff --git a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
index 3a94b873aca..66a31cf85ab 100644
--- a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
+++ b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
@@ -468,7 +468,7 @@ public class TestFunctionsV2 extends TestBase {
   public void test_ST_GeometryN() {
     registerUDFV2("ST_GeometryN", String.class, int.class);
     verifySqlSingleRes(
-        "select ST_AsText(sedona.ST_GeometryN(ST_GeometryFromWKT('MULTIPOINT((10 40), (40 30), (20 20), (30 10))'), 1))",
+        "select ST_AsText(sedona.ST_GeometryN(ST_GeometryFromWKT('MULTIPOINT((10 40), (40 30), (20 20), (30 10))'), 2))",
         "POINT(40 30)");
   }
 
@@ -515,7 +515,7 @@ public class TestFunctionsV2 extends TestBase {
   public void test_ST_InteriorRingN() {
     registerUDFV2("ST_InteriorRingN", String.class, int.class);
     verifySqlSingleRes(
-        "select ST_AsText(sedona.ST_InteriorRingN(ST_GeometryFromWKT('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1), (1 3, 2 3, 2 4, 1 4, 1 3), (3 3, 4 3, 4 4, 3 4, 3 3))'), 0))",
+        "select ST_AsText(sedona.ST_InteriorRingN(ST_GeometryFromWKT('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1), (1 3, 2 3, 2 4, 1 4, 1 3), (3 3, 4 3, 4 4, 3 4, 3 3))'), 1))",
         "LINESTRING(1 1,2 1,2 2,1 2,1 1)");
   }
 
diff --git a/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala b/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
index f66794dd53f..ba219671185 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/PreserveSRIDSuite.scala
@@ -64,8 +64,8 @@ class PreserveSRIDSuite extends TestBaseScala with TableDrivenPropertyChecks {
       ("ST_LineInterpolatePoint(geom3, 0.1)", 1000),
       ("ST_EndPoint(geom3)", 1000),
       ("ST_ExteriorRing(geom1)", 1000),
-      ("ST_GeometryN(geom2, 2)", 1000),
-      ("ST_InteriorRingN(geom4, 0)", 1000),
+      ("ST_GeometryN(geom2, 3)", 1000),
+      ("ST_InteriorRingN(geom4, 1)", 1000),
       ("ST_Dump(geom2)", 1000),
       ("ST_DumpPoints(geom2)", 1000),
       ("ST_AddMeasure(geom3, 0, 1)", 1000),
@@ -82,10 +82,10 @@ class PreserveSRIDSuite extends TestBaseScala with TableDrivenPropertyChecks {
       ("ST_MakeEnvelope(0, 1, 2, 3, 1000)", 1000),
       ("ST_MakeLine(geom3, geom3)", 1000),
       ("ST_Points(geom1)", 1000),
-      ("ST_Polygon(ST_InteriorRingN(geom4, 0), 2000)", 2000),
+      ("ST_Polygon(ST_InteriorRingN(geom4, 1), 2000)", 2000),
       ("ST_Polygonize(geom5)", 1000),
       ("ST_Project(geom5, 12, 12, true)", 1000),
-      ("ST_MakePolygon(ST_ExteriorRing(geom4), ARRAY(ST_InteriorRingN(geom4, 0)))", 1000),
+      ("ST_MakePolygon(ST_ExteriorRing(geom4), ARRAY(ST_InteriorRingN(geom4, 1)))", 1000),
       ("ST_Difference(geom1, geom2)", 1000),
       ("ST_SymDifference(geom1, geom2)", 1000),
       ("ST_UnaryUnion(geom5)", 1000),
diff --git a/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala b/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
index 33e667c571d..34d8c9ca499 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
@@ -1329,7 +1329,7 @@ class dataFrameAPITestScala extends TestBaseScala {
 
     it("Passed ST_GeometryN") {
       val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('MULTIPOINT ((0 0))') AS geom")
-      val df = baseDf.select(ST_GeometryN("geom", 0))
+      val df = baseDf.select(ST_GeometryN("geom", 1))
       val actualResult = df.take(1)(0).get(0).asInstanceOf[Geometry].toText()
       val expectedResult = "POINT (0 0)"
       assert(actualResult == expectedResult)
@@ -1338,7 +1338,7 @@ class dataFrameAPITestScala extends TestBaseScala {
     it("Passed ST_InteriorRingN") {
       val baseDf = sparkSession.sql(
         "SELECT ST_GeomFromWKT('POLYGON ((0 0, 3 0, 3 3, 0 0), (1 1, 2 2, 2 1, 1 1))') AS geom")
-      val df = baseDf.select(ST_InteriorRingN("geom", 0))
+      val df = baseDf.select(ST_InteriorRingN("geom", 1))
       val actualResult = df.take(1)(0).get(0).asInstanceOf[Geometry].toText()
       val expectedResult = "LINESTRING (1 1, 2 2, 2 1, 1 1)"
       assert(actualResult == expectedResult)
diff --git a/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala b/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
index 2b04707248a..04c7f9da7f7 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
@@ -1761,8 +1761,8 @@ class functionTestScala
             |)
             |SELECT
             |  ST_SRID(geom),
-            |  ST_HasZ(ST_GeometryN(ST_GeometryN(geom, 0), 0)),
-            |  ST_HasM(ST_GeometryN(ST_GeometryN(geom, 0), 0))
+            |  ST_HasZ(ST_GeometryN(ST_GeometryN(geom, 1), 1)),
+            |  ST_HasM(ST_GeometryN(ST_GeometryN(geom, 1), 1))
             |FROM shared
             |""".stripMargin)
         .first()
@@ -1886,12 +1886,14 @@ class functionTestScala
   }
 
   it("Should pass ST_GeometryN") {
-    calculateGeometryN("MULTIPOINT((1 2), (3 4), (5 6), (8 9))", 0) shouldBe Some("POINT (1 2)")
-    calculateGeometryN("MULTIPOINT((1 2), (3 4), (5 6), (8 9))", 1) shouldBe Some("POINT (3 4)")
-    calculateGeometryN("MULTIPOINT((1 2), (3 4), (5 6), (8 9))", 2) shouldBe Some("POINT (5 6)")
-    calculateGeometryN("MULTIPOINT((1 2), (3 4), (5 6), (8 9))", 3) shouldBe Some("POINT (8 9)")
-    calculateGeometryN("MULTIPOINT((1 2), (3 4), (5 6), (8 9))", 4) shouldBe None
-    calculateGeometryN("MULTIPOINT((1 2), (3 4), (5 6), (8 9))", 4) shouldBe None
+    // 1-based, as in PostGIS
+    calculateGeometryN("MULTIPOINT((1 2), (3 4), (5 6), (8 9))", 1) shouldBe Some("POINT (1 2)")
+    calculateGeometryN("MULTIPOINT((1 2), (3 4), (5 6), (8 9))", 2) shouldBe Some("POINT (3 4)")
+    calculateGeometryN("MULTIPOINT((1 2), (3 4), (5 6), (8 9))", 3) shouldBe Some("POINT (5 6)")
+    calculateGeometryN("MULTIPOINT((1 2), (3 4), (5 6), (8 9))", 4) shouldBe Some("POINT (8 9)")
+    calculateGeometryN("MULTIPOINT((1 2), (3 4), (5 6), (8 9))", 5) shouldBe None
+    calculateGeometryN("MULTIPOINT((1 2), (3 4), (5 6), (8 9))", 0) shouldBe None
+    calculateGeometryN("MULTIPOINT((1 2), (3 4), (5 6), (8 9))", -1) shouldBe None
 
   }
 
@@ -1906,7 +1908,7 @@ class functionTestScala
       .union(createSampleLineStringsDf(5, "geom"))
 
     When("Using ST_InteriorRingN")
-    val wholes = (0 until 3).flatMap(index =>
+    val wholes = (1 to 3).flatMap(index =>
       polygonDf
         .selectExpr(s"ST_InteriorRingN(geom, $index) as geom")
         .selectExpr("ST_AsText(geom)")
diff --git a/spark/common/src/test/scala/org/apache/sedona/sql/netcdfMetadataTest.scala b/spark/common/src/test/scala/org/apache/sedona/sql/netcdfMetadataTest.scala
index 9ae1efcb99f..b15875d9e99 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/netcdfMetadataTest.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/netcdfMetadataTest.scala
@@ -693,13 +693,13 @@ class netcdfMetadataTest extends TestBaseScala with BeforeAndAfterAll {
         .selectExpr("RS_FromNetCDF(content, 'temp') as raster")
         .selectExpr(
           "RS_Metadata(raster) as metadata",
-          "RS_Value(raster, 0, 0, 1) as upperLeft",
-          "RS_Value(raster, 2, 0, 1) as upperRight",
-          "RS_Value(raster, 0, 2, 1) as lowerLeft",
-          "RS_Value(raster, 2, 2, 1) as lowerRight",
-          "RS_Value(raster, 1, 0, 1) as distinctMissing",
-          "RS_Value(raster, 1, 1, 1) as centerNoData",
-          "RS_Value(raster, 1, 2, 1) as validCollision",
+          "RS_Value(raster, 1, 1, 1) as upperLeft",
+          "RS_Value(raster, 3, 1, 1) as upperRight",
+          "RS_Value(raster, 1, 3, 1) as lowerLeft",
+          "RS_Value(raster, 3, 3, 1) as lowerRight",
+          "RS_Value(raster, 2, 1, 1) as distinctMissing",
+          "RS_Value(raster, 2, 2, 1) as centerNoData",
+          "RS_Value(raster, 2, 3, 1) as validCollision",
           "RS_Count(raster, 1) as validCount",
           "RS_BandNoDataValue(raster, 1) as noData")
         .first()
@@ -856,11 +856,11 @@ class netcdfMetadataTest extends TestBaseScala with BeforeAndAfterAll {
           "RS_Width(raster) as width",
           "RS_Height(raster) as height",
           "RS_NumBands(raster) as numBands",
-          "RS_Value(raster, 0, 0, 1) as upperLeftBand1",
-          "RS_Value(raster, 0, 0, 2) as upperLeftBand2",
-          "RS_Value(raster, 3, 0, 1) as upperRightBand1",
-          "RS_Value(raster, 0, 2, 1) as lowerLeftBand1",
-          "RS_Value(raster, 3, 2, 1) as lowerRightBand1",
+          "RS_Value(raster, 1, 1, 1) as upperLeftBand1",
+          "RS_Value(raster, 1, 1, 2) as upperLeftBand2",
+          "RS_Value(raster, 4, 1, 1) as upperRightBand1",
+          "RS_Value(raster, 1, 3, 1) as lowerLeftBand1",
+          "RS_Value(raster, 4, 3, 1) as lowerRightBand1",
           "RS_Metadata(raster) as metadata")
         .first()
       assertEquals(4, row.getAs[Int]("width"))
@@ -889,9 +889,9 @@ class netcdfMetadataTest extends TestBaseScala with BeforeAndAfterAll {
           "RS_Width(raster) as width",
           "RS_Height(raster) as height",
           "RS_NumBands(raster) as numBands",
-          "RS_Value(raster, 0, 0, 1) as upperLeftBand1",
-          "RS_Value(raster, 0, 0, 2) as upperLeftBand2",
-          "RS_Value(raster, 1, 2, 1) as lowerRightBand1")
+          "RS_Value(raster, 1, 1, 1) as upperLeftBand1",
+          "RS_Value(raster, 1, 1, 2) as upperLeftBand2",
+          "RS_Value(raster, 2, 3, 1) as lowerRightBand1")
         .first()
       assertEquals(2, row.getAs[Int]("width"))
       assertEquals(3, row.getAs[Int]("height"))
diff --git a/spark/common/src/test/scala/org/apache/sedona/sql/ogc/GeometryTypesAndFunctionsTestScala.scala b/spark/common/src/test/scala/org/apache/sedona/sql/ogc/GeometryTypesAndFunctionsTestScala.scala
index 112a896bfd5..ef799548a55 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/ogc/GeometryTypesAndFunctionsTestScala.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/ogc/GeometryTypesAndFunctionsTestScala.scala
@@ -391,9 +391,7 @@ class GeometryTypesAndFunctionsTestScala extends TestBaseScala with BeforeAndAft
         .getInt(0)
       assert(actual == 1)
     }
-    ignore("T29") {
-      // Interior rings are 1 indexed in OGC and PostGIS.
-      // In Sedona they are 0 indexed.
+    it("T29") {
       val actual = sparkSession
         .sql("""
           |SELECT ST_AsText(ST_InteriorRingN(shore, 1))
@@ -415,9 +413,7 @@ class GeometryTypesAndFunctionsTestScala extends TestBaseScala with BeforeAndAft
         .getInt(0)
       assert(actual == 2)
     }
-    ignore("T31") {
-      // Interior rings are 1 indexed in OGC and PostGIS.
-      // In Sedona they are 0 indexed.
+    it("T31") {
       val actual = sparkSession
         .sql("""
           |SELECT ST_AsText(ST_GeometryN(centerlines, 2))
diff --git a/spark/common/src/test/scala/org/apache/sedona/sql/rasteralgebraTest.scala b/spark/common/src/test/scala/org/apache/sedona/sql/rasteralgebraTest.scala
index 7a48fa7b589..eb2a9f676a8 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/rasteralgebraTest.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/rasteralgebraTest.scala
@@ -1014,7 +1014,7 @@ class rasteralgebraTest extends TestBaseScala with BeforeAndAfter with GivenWhen
     it("Passed RS_Value with raster and coordinates") {
       val df = sparkSession.read.format("binaryFile").load(resourceFolder + "raster/test1.tiff")
       val result =
-        df.selectExpr("RS_Value(RS_FromGeoTiff(content), 4, 4, 1)").first().getDouble(0)
+        df.selectExpr("RS_Value(RS_FromGeoTiff(content), 5, 5, 1)").first().getDouble(0)
       assert(result == 123d)
     }
 
@@ -1086,7 +1086,7 @@ class rasteralgebraTest extends TestBaseScala with BeforeAndAfter with GivenWhen
     it("Passed RS_Values with raster and Grid Coordinates") {
       val df = sparkSession.read.format("binaryFile").load(resourceFolder + "raster/test1.tiff")
       val result = df
-        .selectExpr("RS_Values(RS_FromGeoTiff(content), array(1,2), array(3,2), 1)")
+        .selectExpr("RS_Values(RS_FromGeoTiff(content), array(2,3), array(4,3), 1)")
         .first()
         .getList[Double](0)
 
@@ -2920,7 +2920,7 @@ class rasteralgebraTest extends TestBaseScala with BeforeAndAfter with GivenWhen
 
       val expectedFirstVal = 60.95357131958008
       val actualFirstVal =
-        rasterDf.selectExpr("RS_Value(raster, 0, 0, 1) as raster").first().getDouble(0)
+        rasterDf.selectExpr("RS_Value(raster, 1, 1, 1) as raster").first().getDouble(0)
       assertEquals(expectedFirstVal, actualFirstVal, 1e-6)
     }
 
@@ -2937,7 +2937,7 @@ class rasteralgebraTest extends TestBaseScala with BeforeAndAfter with GivenWhen
 
       val expectedFirstVal = 60.95357131958008
       val actualFirstVal =
-        rasterDf.selectExpr("RS_Value(raster, 0, 0, 1) as raster").first().getDouble(0)
+        rasterDf.selectExpr("RS_Value(raster, 1, 1, 1) as raster").first().getDouble(0)
       assertEquals(expectedFirstVal, actualFirstVal, 1e-6)
     }