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

jiayu 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 f265c9e5af [GH-2272] add new constructor ST_GeogFromGeoHash (#2271)
f265c9e5af is described below

commit f265c9e5af08a3043421285ff9cdfb6ee55e1127
Author: Zhuocheng Shang <[email protected]>
AuthorDate: Thu Aug 14 11:45:52 2025 -0700

    [GH-2272] add new constructor ST_GeogFromGeoHash (#2271)
    
    * add new constructor ST_GeogFromGeoHash
    
    * fix constructor api doc
    
    * fix en-of-file in api doc
    
    * fix SRID output for ST_GeogGeoHash
    
    * add back ST_GeogFromWKT header
    
    * resolve conflicts
    
    * Update docs/api/sql/geography/Constructor.md
    
    ---------
    
    Co-authored-by: Jia Yu <[email protected]>
---
 .../sedona/common/geography/Constructors.java      | 12 ++++++++++
 .../apache/sedona/common/utils/GeoHashDecoder.java | 26 ++++++++++++++++++++++
 .../sedona/common/Geography/ConstructorsTest.java  | 16 +++++++++++++
 docs/api/sql/geography/Constructor.md              | 22 ++++++++++++++++++
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala  |  3 +++
 .../expressions/geography/Constructors.scala       | 15 ++++++++++++-
 .../sedona_sql/expressions/st_constructors.scala   | 12 ++++++++++
 .../geography/ConstructorsDataFrameAPITest.scala   | 11 +++++++++
 .../sedona/sql/geography/ConstructorsTest.scala    | 21 +++++++++++++++++
 9 files changed, 137 insertions(+), 1 deletion(-)

diff --git 
a/common/src/main/java/org/apache/sedona/common/geography/Constructors.java 
b/common/src/main/java/org/apache/sedona/common/geography/Constructors.java
index 5f70b1daca..6efbd8e0b6 100644
--- a/common/src/main/java/org/apache/sedona/common/geography/Constructors.java
+++ b/common/src/main/java/org/apache/sedona/common/geography/Constructors.java
@@ -18,9 +18,11 @@
  */
 package org.apache.sedona.common.geography;
 
+import java.io.IOException;
 import org.apache.sedona.common.S2Geography.Geography;
 import org.apache.sedona.common.S2Geography.WKBReader;
 import org.apache.sedona.common.S2Geography.WKTReader;
+import org.apache.sedona.common.utils.GeoHashDecoder;
 import org.locationtech.jts.io.ParseException;
 
 public class Constructors {
@@ -67,4 +69,14 @@ public class Constructors {
     }
     return geogFromWKT(wkt, srid);
   }
+
+  public static Geography geogFromGeoHash(String geoHash, Integer precision) {
+    try {
+      return GeoHashDecoder.decodeGeog(geoHash, precision);
+    } catch (GeoHashDecoder.InvalidGeoHashException e) {
+      return null;
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
 }
diff --git 
a/common/src/main/java/org/apache/sedona/common/utils/GeoHashDecoder.java 
b/common/src/main/java/org/apache/sedona/common/utils/GeoHashDecoder.java
index 7662cd8fb9..b28070310c 100644
--- a/common/src/main/java/org/apache/sedona/common/utils/GeoHashDecoder.java
+++ b/common/src/main/java/org/apache/sedona/common/utils/GeoHashDecoder.java
@@ -18,6 +18,11 @@
  */
 package org.apache.sedona.common.utils;
 
+import com.google.common.geometry.*;
+import java.io.IOException;
+import java.util.List;
+import org.apache.sedona.common.S2Geography.Geography;
+import org.apache.sedona.common.S2Geography.PolygonGeography;
 import org.locationtech.jts.geom.Geometry;
 
 public class GeoHashDecoder {
@@ -34,6 +39,27 @@ public class GeoHashDecoder {
     return decodeGeoHashBBox(geohash, precision).getBbox().toPolygon();
   }
 
+  public static Geography decodeGeog(String geohash, Integer precision)
+      throws InvalidGeoHashException, IOException {
+    BBox box = decodeGeoHashBBox(geohash, precision).getBbox(); // 
west,east,south,north
+    double south = box.startLat, north = box.endLat;
+    double west = box.startLon, east = box.endLon;
+
+    // 4 corners (lat, lon) in CCW order: SW → SE → NE → NW
+    S2Point SW = S2LatLng.fromDegrees(south, west).toPoint();
+    S2Point SE = S2LatLng.fromDegrees(south, east).toPoint();
+    S2Point NE = S2LatLng.fromDegrees(north, east).toPoint();
+    S2Point NW = S2LatLng.fromDegrees(north, west).toPoint();
+
+    S2Loop shell = new S2Loop(List.of(SW, SE, NE, NW));
+    shell.normalize(); // ensure CCW & smaller-area orientation (safe even if 
already CCW)
+
+    S2Polygon s2poly = new S2Polygon(shell);
+    Geography geog = new PolygonGeography(s2poly);
+    geog.setSRID(4326);
+    return geog;
+  }
+
   private static class LatLon {
     public Double[] lons;
 
diff --git 
a/common/src/test/java/org/apache/sedona/common/Geography/ConstructorsTest.java 
b/common/src/test/java/org/apache/sedona/common/Geography/ConstructorsTest.java
index 4d2075920e..8198500bb7 100644
--- 
a/common/src/test/java/org/apache/sedona/common/Geography/ConstructorsTest.java
+++ 
b/common/src/test/java/org/apache/sedona/common/Geography/ConstructorsTest.java
@@ -28,6 +28,7 @@ import org.apache.sedona.common.S2Geography.WKBReader;
 import org.apache.sedona.common.S2Geography.WKBWriter;
 import org.apache.sedona.common.geography.Constructors;
 import org.junit.Test;
+import org.locationtech.jts.geom.PrecisionModel;
 import org.locationtech.jts.io.ParseException;
 
 public class ConstructorsTest {
@@ -123,4 +124,19 @@ public class ConstructorsTest {
     assertEquals(expectedGeom, result.toString());
     assertEquals(4326, result.getSRID());
   }
+
+  @Test
+  public void geogFromGeoHash() throws ParseException {
+    Geography geog = Constructors.geogFromGeoHash("9q9j8ue2v71y5zzy0s4q", 16);
+    String expectedWkt =
+        "SRID=4326; POLYGON ((-122.3061 37.554162, -122.3061 37.554162, 
-122.3061 37.554162, -122.3061 37.554162, -122.3061 37.554162))";
+    String actualWkt = geog.toText(new PrecisionModel(1e6));
+    assertEquals(expectedWkt, actualWkt);
+
+    geog = Constructors.geogFromGeoHash("s00twy01mt", 4);
+    expectedWkt =
+        "SRID=4326; POLYGON ((0.703125 0.8789062, 1.0546875 0.8789062, 
1.0546875 1.0546875, 0.703125 1.0546875, 0.703125 0.8789062))";
+    actualWkt = geog.toText(new PrecisionModel(1e6));
+    assertEquals(expectedWkt, actualWkt);
+  }
 }
diff --git a/docs/api/sql/geography/Constructor.md 
b/docs/api/sql/geography/Constructor.md
index 8cf2d9b74d..bd5577c8f4 100644
--- a/docs/api/sql/geography/Constructor.md
+++ b/docs/api/sql/geography/Constructor.md
@@ -61,6 +61,28 @@ Output:
 SRID: 4326; LINESTRING (-2.1 -0.4, -1.5 -0.7)
 ```
 
+## ST_GeogFromGeoHash
+
+Introduction: Create Geography from geohash string and optional precision
+
+Format:
+
+`ST_GeogFromGeoHash(geohash: String, precision: Integer)`
+
+Since: `v1.8.0`
+
+SQL Example
+
+```sql
+SELECT ST_GeogFromGeoHash('9q9j8ue2v71y5zzy0s4q', 16)
+```
+
+Output:
+
+```
+SRID=4326; POLYGON ((-122.3061 37.554162, -122.3061 37.554162, -122.3061 
37.554162, -122.3061 37.554162, -122.3061 37.554162))"
+```
+
 ## ST_GeogFromWKT
 
 Introduction: Construct a Geography from WKT. If SRID is not set, it defaults 
to 0 (unknown).
diff --git 
a/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala 
b/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
index 4b948c63a5..074db478d0 100644
--- a/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
+++ b/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
@@ -22,6 +22,8 @@ import org.apache.spark.sql.expressions.Aggregator
 import org.apache.spark.sql.sedona_sql.expressions.collect.ST_Collect
 import org.apache.spark.sql.sedona_sql.expressions.raster._
 import org.apache.spark.sql.sedona_sql.expressions._
+import 
org.apache.spark.sql.sedona_sql.expressions.geography.{ST_GeogCollFromText, 
ST_GeogFromGeoHash, ST_GeogFromText, ST_GeogFromWKB, ST_GeogFromWKT}
+import 
org.apache.spark.sql.sedona_sql.expressions.geography.{ST_GeogCollFromText, 
ST_GeogFromEWKT, ST_GeogFromText, ST_GeogFromWKB, ST_GeogFromWKT}
 import 
org.apache.spark.sql.sedona_sql.expressions.geography.{ST_GeogCollFromText, 
ST_GeogFromEWKB, ST_GeogFromEWKT, ST_GeogFromText, ST_GeogFromWKB, 
ST_GeogFromWKT}
 import org.locationtech.jts.geom.Geometry
 import org.locationtech.jts.operation.buffer.BufferParameters
@@ -188,6 +190,7 @@ object Catalog extends AbstractCatalog {
     function[ST_GeoHash](),
     function[ST_GeomFromGeoHash](null),
     function[ST_PointFromGeoHash](null),
+    function[ST_GeogFromGeoHash](null),
     function[ST_Collect](),
     function[ST_Multi](),
     function[ST_PointOnSurface](),
diff --git 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/geography/Constructors.scala
 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/geography/Constructors.scala
index 13eb40ee81..ad339c146b 100644
--- 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/geography/Constructors.scala
+++ 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/geography/Constructors.scala
@@ -21,7 +21,7 @@ package org.apache.spark.sql.sedona_sql.expressions.geography
 import org.apache.sedona.common.geography.Constructors
 import org.apache.spark.sql.catalyst.expressions.Expression
 import 
org.apache.spark.sql.sedona_sql.expressions.InferrableFunctionConverter._
-import org.apache.spark.sql.sedona_sql.expressions.InferredExpression
+import org.apache.spark.sql.sedona_sql.expressions.{InferrableFunction, 
InferredExpression}
 
 /**
  * Return a Geography from a WKT string
@@ -108,3 +108,16 @@ private[apache] case class 
ST_GeogFromEWKB(inputExpressions: Seq[Expression])
     copy(inputExpressions = newChildren)
   }
 }
+
+/**
+ * Return a Geography from a GeoHash string
+ *
+ * @param inputExpressions
+ *   This function takes a geography geohash
+ */
+private[apache] case class ST_GeogFromGeoHash(inputExpressions: 
Seq[Expression])
+    extends 
InferredExpression(InferrableFunction.allowRightNull(Constructors.geogFromGeoHash))
 {
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
diff --git 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_constructors.scala
 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_constructors.scala
index 7dcfa2b115..38a5469cf4 100644
--- 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_constructors.scala
+++ 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_constructors.scala
@@ -22,6 +22,7 @@ import org.apache.spark.sql.Column
 import org.apache.spark.sql.sedona_sql.DataFrameShims._
 import 
org.apache.spark.sql.sedona_sql.expressions.geography.{ST_GeogCollFromText, 
ST_GeogFromEWKB, ST_GeogFromText, ST_GeogFromWKB, ST_GeogFromWKT}
 import 
org.apache.spark.sql.sedona_sql.expressions.geography.{ST_GeogCollFromText, 
ST_GeogFromEWKT, ST_GeogFromText, ST_GeogFromWKB, ST_GeogFromWKT}
+import 
org.apache.spark.sql.sedona_sql.expressions.geography.{ST_GeogCollFromText, 
ST_GeogFromGeoHash, ST_GeogFromText, ST_GeogFromWKB, ST_GeogFromWKT}
 
 object st_constructors {
   def ST_GeomFromGeoHash(geohash: Column, precision: Column): Column =
@@ -35,6 +36,17 @@ object st_constructors {
   def ST_GeomFromGeoHash(geohash: String): Column =
     wrapExpression[ST_GeomFromGeoHash](geohash, null)
 
+  def ST_GeogFromGeoHash(geohash: Column, precision: Column): Column =
+    wrapExpression[ST_GeogFromGeoHash](geohash, precision)
+  def ST_GeogFromGeoHash(geohash: String, precision: Int): Column =
+    wrapExpression[ST_GeogFromGeoHash](geohash, precision)
+
+  def ST_GeogFromGeoHash(geohash: Column): Column =
+    wrapExpression[ST_GeogFromGeoHash](geohash, null)
+
+  def ST_GeogFromGeoHash(geohash: String): Column =
+    wrapExpression[ST_GeogFromGeoHash](geohash, null)
+
   def ST_PointFromGeoHash(geohash: Column, precision: Column): Column =
     wrapExpression[ST_PointFromGeoHash](geohash, precision)
   def ST_PointFromGeoHash(geohash: String, precision: Int): Column =
diff --git 
a/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsDataFrameAPITest.scala
 
b/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsDataFrameAPITest.scala
index 24625e9445..9417e4eed8 100644
--- 
a/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsDataFrameAPITest.scala
+++ 
b/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsDataFrameAPITest.scala
@@ -85,4 +85,15 @@ class ConstructorsDataFrameAPITest extends TestBaseScala {
     assert(actualResult.getSRID == 4269)
   }
 
+  it("Passed ST_GeogFromGeoHash") {
+    val df = sparkSession
+      .sql("SELECT '9q9j8ue2v71y5zzy0s4q' AS geohash")
+      .select(st_constructors.ST_GeogFromGeoHash("geohash", 16))
+    val actualResult =
+      df.take(1)(0).get(0).asInstanceOf[Geography].toText(new 
PrecisionModel(1e6))
+    var expectedWkt =
+      "SRID=4326; POLYGON ((-122.3061 37.554162, -122.3061 37.554162, 
-122.3061 37.554162, -122.3061 37.554162, -122.3061 37.554162))"
+    assertEquals(expectedWkt, actualResult)
+  }
+
 }
diff --git 
a/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsTest.scala
 
b/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsTest.scala
index 6f4a9b376d..092355a691 100644
--- 
a/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsTest.scala
+++ 
b/spark/common/src/test/scala/org/apache/sedona/sql/geography/ConstructorsTest.scala
@@ -19,7 +19,9 @@
 package org.apache.sedona.sql.geography
 
 import org.apache.sedona.common.S2Geography.Geography
+import org.apache.sedona.common.geography.Constructors
 import org.apache.sedona.sql.TestBaseScala
+import org.junit.Assert.assertEquals
 import org.locationtech.jts.geom.PrecisionModel
 
 class ConstructorsTest extends TestBaseScala {
@@ -27,6 +29,25 @@ class ConstructorsTest extends TestBaseScala {
   import sparkSession.implicits._
   val precisionModel: PrecisionModel = new 
PrecisionModel(PrecisionModel.FIXED);
 
+  it("Passed ST_GeogFromGeoHash") {
+    var geohash = "9q9j8ue2v71y5zzy0s4q";
+    var precision = 16;
+    var row =
+      sparkSession.sql(s"SELECT ST_GeogFromGeoHash('$geohash','$precision') AS 
geog").first()
+    var geoStr = row.get(0).asInstanceOf[Geography].toText(new 
PrecisionModel(1e6))
+    var expectedWkt =
+      "SRID=4326; POLYGON ((-122.3061 37.554162, -122.3061 37.554162, 
-122.3061 37.554162, -122.3061 37.554162, -122.3061 37.554162))"
+    assertEquals(expectedWkt, geoStr)
+
+    geohash = "s00twy01mt"
+    precision = 4;
+    row = sparkSession.sql(s"SELECT 
ST_GeogFromGeoHash('$geohash','$precision') AS geog").first()
+    geoStr = row.get(0).asInstanceOf[Geography].toText(new PrecisionModel(1e6))
+    expectedWkt =
+      "SRID=4326; POLYGON ((0.703125 0.8789062, 1.0546875 0.8789062, 1.0546875 
1.0546875, 0.703125 1.0546875, 0.703125 0.8789062))"
+    assertEquals(expectedWkt, geoStr)
+  }
+
   it("Passed ST_GeogFromWKT") {
     val wkt = "LINESTRING (1 2, 3 4, 5 6)"
     val wktExpected = "SRID=4326; LINESTRING (1 2, 3 4, 5 6)"

Reply via email to