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 a648f0c7b5 [GH-1327] Add Bing Tile functions (#2668)
a648f0c7b5 is described below

commit a648f0c7b53e834c366a3c508d66ab6e8976212e
Author: Jia Yu <[email protected]>
AuthorDate: Sat Feb 21 12:59:07 2026 -0700

    [GH-1327] Add Bing Tile functions (#2668)
---
 .../java/org/apache/sedona/common/Functions.java   | 125 +++++
 .../org/apache/sedona/common/utils/BingTile.java   | 586 +++++++++++++++++++++
 .../org/apache/sedona/common/FunctionsTest.java    | 311 +++++++++++
 docs/api/flink/Function.md                         | 183 +++++++
 docs/api/snowflake/vector-data/Function.md         | 162 ++++++
 docs/api/sql/Function.md                           | 183 +++++++
 .../main/java/org/apache/sedona/flink/Catalog.java |   9 +
 .../apache/sedona/flink/expressions/Functions.java |  89 ++++
 .../java/org/apache/sedona/flink/FunctionTest.java | 178 +++++++
 python/sedona/spark/sql/st_functions.py            | 137 +++++
 .../sedona/snowflake/snowsql/TestFunctions.java    |  60 +++
 .../sedona/snowflake/snowsql/TestFunctionsV2.java  |  60 +++
 .../org/apache/sedona/snowflake/snowsql/UDFs.java  |  48 ++
 .../apache/sedona/snowflake/snowsql/UDFsV2.java    |  61 +++
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala  |   9 +
 .../sql/sedona_sql/expressions/Functions.scala     |  77 +++
 .../sql/sedona_sql/expressions/st_functions.scala  |  40 ++
 .../apache/sedona/sql/dataFrameAPITestScala.scala  | 107 ++++
 .../sedona/sql/functions/STBingTileFunctions.scala | 254 +++++++++
 19 files changed, 2679 insertions(+)

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 22b9a2fbf4..76bea7e695 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -1815,6 +1815,131 @@ public class Functions {
     return polygons.toArray(new Polygon[0]);
   }
 
+  // =========================================================================
+  // Bing Tile functions
+  // =========================================================================
+
+  /**
+   * Creates a Bing tile quadkey from tile XY coordinates and zoom level.
+   *
+   * @param tileX the tile X coordinate
+   * @param tileY the tile Y coordinate
+   * @param zoomLevel the zoom level (1 to 23)
+   * @return the quadkey string
+   */
+  public static String bingTile(int tileX, int tileY, int zoomLevel) {
+    return BingTile.fromCoordinates(tileX, tileY, zoomLevel).toQuadKey();
+  }
+
+  /**
+   * Returns the Bing tile quadkey at a given zoom level containing the 
specified point.
+   *
+   * @param longitude the longitude (-180 to 180)
+   * @param latitude the latitude (-85.05112878 to 85.05112878)
+   * @param zoomLevel the zoom level (1 to 23)
+   * @return the quadkey string
+   */
+  public static String bingTileAt(double longitude, double latitude, int 
zoomLevel) {
+    return BingTile.fromLatLon(latitude, longitude, zoomLevel).toQuadKey();
+  }
+
+  /**
+   * Returns the 3x3 neighborhood of Bing tiles around the tile containing the 
specified point.
+   *
+   * @param longitude the longitude
+   * @param latitude the latitude
+   * @param zoomLevel the zoom level (1 to 23)
+   * @return array of quadkey strings
+   */
+  public static String[] bingTilesAround(double longitude, double latitude, 
int zoomLevel) {
+    List<BingTile> tiles = BingTile.tilesAround(latitude, longitude, 
zoomLevel);
+    return tiles.stream().map(BingTile::toQuadKey).toArray(String[]::new);
+  }
+
+  /**
+   * Returns the zoom level of a Bing tile quadkey.
+   *
+   * @param quadKey the quadkey string
+   * @return the zoom level
+   */
+  public static int bingTileZoomLevel(String quadKey) {
+    // Validate the quadkey by parsing it
+    BingTile.fromQuadKey(quadKey);
+    return quadKey.length();
+  }
+
+  /**
+   * Returns the X coordinate of a Bing tile from its quadkey.
+   *
+   * @param quadKey the quadkey string
+   * @return the tile X coordinate
+   */
+  public static int bingTileX(String quadKey) {
+    return BingTile.fromQuadKey(quadKey).getX();
+  }
+
+  /**
+   * Returns the Y coordinate of a Bing tile from its quadkey.
+   *
+   * @param quadKey the quadkey string
+   * @return the tile Y coordinate
+   */
+  public static int bingTileY(String quadKey) {
+    return BingTile.fromQuadKey(quadKey).getY();
+  }
+
+  /**
+   * Returns the polygon representation of a Bing tile given its quadkey.
+   *
+   * @param quadKey the quadkey string
+   * @return the tile polygon
+   */
+  public static Geometry bingTilePolygon(String quadKey) {
+    return BingTile.fromQuadKey(quadKey).toPolygon();
+  }
+
+  /**
+   * Validates and normalizes a Bing tile quadkey string.
+   *
+   * <p>The input quadkey is parsed into a {@code BingTile} for validation, 
and the corresponding
+   * (possibly normalized) quadkey string is returned. Invalid quadkeys will 
cause {@code
+   * BingTile.fromQuadKey} to throw an exception.
+   *
+   * @param quadKey the quadkey string to validate and normalize
+   * @return the validated and normalized quadkey string
+   */
+  public static String bingTileQuadKey(String quadKey) {
+    // Validate and return. This serves as a validation/normalization function.
+    return BingTile.fromQuadKey(quadKey).toQuadKey();
+  }
+
+  /**
+   * Returns the minimum set of Bing tile quadkeys that fully cover a given 
geometry at the
+   * specified zoom level.
+   *
+   * @param geometry the geometry to cover
+   * @param zoomLevel the zoom level (1 to 23)
+   * @return array of quadkey strings
+   */
+  public static String[] bingTileCellIDs(Geometry geometry, int zoomLevel) {
+    List<BingTile> tiles = BingTile.tilesCovering(geometry, zoomLevel);
+    return tiles.stream().map(BingTile::toQuadKey).toArray(String[]::new);
+  }
+
+  /**
+   * Converts an array of Bing tile quadkeys to their polygon geometries.
+   *
+   * @param quadKeys the array of quadkey strings
+   * @return array of polygon geometries
+   */
+  public static Geometry[] bingTileToGeom(String[] quadKeys) {
+    Geometry[] polygons = new Geometry[quadKeys.length];
+    for (int i = 0; i < quadKeys.length; i++) {
+      polygons[i] = BingTile.fromQuadKey(quadKeys[i]).toPolygon();
+    }
+    return polygons;
+  }
+
   public static Geometry simplify(Geometry geom, double distanceTolerance) {
     return DouglasPeuckerSimplifier.simplify(geom, distanceTolerance);
   }
diff --git a/common/src/main/java/org/apache/sedona/common/utils/BingTile.java 
b/common/src/main/java/org/apache/sedona/common/utils/BingTile.java
new file mode 100644
index 0000000000..d3ebf30c93
--- /dev/null
+++ b/common/src/main/java/org/apache/sedona/common/utils/BingTile.java
@@ -0,0 +1,586 @@
+/*
+ * 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.
+ */
+package org.apache.sedona.common.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.Polygon;
+
+/**
+ * Represents a Bing Maps tile. Bing Maps uses a quadtree-based tiling system 
where tiles are
+ * identified by (x, y, zoomLevel) coordinates or equivalently by a quadkey 
string.
+ *
+ * <p>The implementation is based on the Bing Maps Tile System specification:
+ * https://docs.microsoft.com/en-us/bingmaps/articles/bing-maps-tile-system
+ *
+ * <p>Ported from https://github.com/wwbrannon/bing-tile-hive (Apache-2.0 
license), which itself is
+ * a port of Presto's Bing Tile implementation. Adapted for JTS geometry.
+ */
+public final class BingTile {
+
+  public static final int MAX_ZOOM_LEVEL = 23;
+  private static final int TILE_PIXELS = 256;
+  private static final double MAX_LATITUDE = 85.05112878;
+  private static final double MIN_LATITUDE = -85.05112878;
+  private static final double MAX_LONGITUDE = 180;
+  private static final double MIN_LONGITUDE = -180;
+
+  private static final int OPTIMIZED_TILING_MIN_ZOOM_LEVEL = 10;
+
+  private static final String LATITUDE_OUT_OF_RANGE =
+      "Latitude must be between " + MIN_LATITUDE + " and " + MAX_LATITUDE;
+  private static final String LONGITUDE_OUT_OF_RANGE =
+      "Longitude must be between " + MIN_LONGITUDE + " and " + MAX_LONGITUDE;
+  private static final String QUAD_KEY_EMPTY = "QuadKey must not be empty 
string";
+  private static final String QUAD_KEY_TOO_LONG =
+      "QuadKey must be " + MAX_ZOOM_LEVEL + " characters or less";
+  private static final String ZOOM_LEVEL_TOO_SMALL = "Zoom level must be > 0";
+  private static final String ZOOM_LEVEL_TOO_LARGE = "Zoom level must be <= " 
+ MAX_ZOOM_LEVEL;
+
+  private static final GeometryFactory GEOMETRY_FACTORY = new 
GeometryFactory();
+
+  private final int x;
+  private final int y;
+  private final int zoomLevel;
+
+  // =========================================================================
+  // Constructors
+  // =========================================================================
+
+  private BingTile(int x, int y, int zoomLevel) {
+    checkZoomLevel(zoomLevel);
+    checkCoordinate(x, zoomLevel);
+    checkCoordinate(y, zoomLevel);
+    this.x = x;
+    this.y = y;
+    this.zoomLevel = zoomLevel;
+  }
+
+  // =========================================================================
+  // Validation methods
+  // =========================================================================
+
+  private static void checkCondition(boolean condition, String formatString, 
Object... args) {
+    if (!condition) {
+      throw new IllegalArgumentException(String.format(formatString, args));
+    }
+  }
+
+  private static void checkZoomLevel(long zoomLevel) {
+    checkCondition(zoomLevel > 0, ZOOM_LEVEL_TOO_SMALL);
+    checkCondition(zoomLevel <= MAX_ZOOM_LEVEL, ZOOM_LEVEL_TOO_LARGE);
+  }
+
+  private static void checkCoordinate(long coordinate, long zoomLevel) {
+    checkCondition(
+        coordinate >= 0 && coordinate < (1 << zoomLevel),
+        "XY coordinates for a Bing tile at zoom level %s must be within [0, 
%s) range",
+        zoomLevel,
+        1 << zoomLevel);
+  }
+
+  private static void checkQuadKey(String quadkey) {
+    checkCondition(quadkey.length() > 0, QUAD_KEY_EMPTY);
+    checkCondition(quadkey.length() <= MAX_ZOOM_LEVEL, QUAD_KEY_TOO_LONG);
+  }
+
+  private static void checkLatitude(double latitude) {
+    checkCondition(latitude >= MIN_LATITUDE && latitude <= MAX_LATITUDE, 
LATITUDE_OUT_OF_RANGE);
+  }
+
+  private static void checkLongitude(double longitude) {
+    checkCondition(
+        longitude >= MIN_LONGITUDE && longitude <= MAX_LONGITUDE, 
LONGITUDE_OUT_OF_RANGE);
+  }
+
+  // =========================================================================
+  // Utility functions for converting to/from latitude/longitude
+  // =========================================================================
+
+  private static long mapSize(int zoomLevel) {
+    return 256L << zoomLevel;
+  }
+
+  private static double clip(double n, double minValue, double maxValue) {
+    return Math.min(Math.max(n, minValue), maxValue);
+  }
+
+  private static int axisToCoordinates(double axis, long mapSize) {
+    int tileAxis = (int) clip(axis * mapSize, 0, mapSize - 1);
+    return tileAxis / TILE_PIXELS;
+  }
+
+  private static int longitudeToTileX(double longitude, long mapSize) {
+    double x = (longitude + 180) / 360;
+    return axisToCoordinates(x, mapSize);
+  }
+
+  private static int latitudeToTileY(double latitude, long mapSize) {
+    double sinLatitude = Math.sin(latitude * Math.PI / 180);
+    double y = 0.5 - Math.log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * 
Math.PI);
+    return axisToCoordinates(y, mapSize);
+  }
+
+  /**
+   * Converts tile XY coordinates to the upper-left corner latitude/longitude 
of the tile.
+   *
+   * @return a Coordinate with x=longitude, y=latitude
+   */
+  private static Coordinate tileXYToLatitudeLongitude(int tileX, int tileY, 
int zoomLevel) {
+    long mapSize = mapSize(zoomLevel);
+    double x = (clip(tileX * TILE_PIXELS, 0, mapSize) / mapSize) - 0.5;
+    double y = 0.5 - (clip(tileY * TILE_PIXELS, 0, mapSize) / mapSize);
+    double lat = 90 - 360 * Math.atan(Math.exp(-y * 2 * Math.PI)) / Math.PI;
+    double lon = 360 * x;
+    return new Coordinate(lon, lat);
+  }
+
+  // =========================================================================
+  // Factory methods
+  // =========================================================================
+
+  /**
+   * Creates a BingTile from XY coordinates and zoom level.
+   *
+   * @param x the tile X coordinate
+   * @param y the tile Y coordinate
+   * @param zoomLevel the zoom level (1 to 23)
+   * @return a new BingTile
+   */
+  public static BingTile fromCoordinates(int x, int y, int zoomLevel) {
+    return new BingTile(x, y, zoomLevel);
+  }
+
+  /**
+   * Creates a BingTile from a latitude/longitude point at the given zoom 
level.
+   *
+   * @param latitude the latitude (-85.05112878 to 85.05112878)
+   * @param longitude the longitude (-180 to 180)
+   * @param zoomLevel the zoom level (1 to 23)
+   * @return a new BingTile
+   */
+  public static BingTile fromLatLon(double latitude, double longitude, int 
zoomLevel) {
+    checkLatitude(latitude);
+    checkLongitude(longitude);
+    checkZoomLevel(zoomLevel);
+
+    long mapSize = mapSize(zoomLevel);
+    int tileX = longitudeToTileX(longitude, mapSize);
+    int tileY = latitudeToTileY(latitude, mapSize);
+    return new BingTile(tileX, tileY, zoomLevel);
+  }
+
+  /**
+   * Creates a BingTile from a quadkey string.
+   *
+   * @param quadKey the quadkey string (e.g. "0231")
+   * @return a new BingTile
+   */
+  public static BingTile fromQuadKey(String quadKey) {
+    checkQuadKey(quadKey);
+
+    int zoomLevel = quadKey.length();
+    checkZoomLevel(zoomLevel);
+
+    int tileX = 0;
+    int tileY = 0;
+
+    for (int i = zoomLevel; i > 0; i--) {
+      int mask = 1 << (i - 1);
+      switch (quadKey.charAt(zoomLevel - i)) {
+        case '0':
+          break;
+        case '1':
+          tileX |= mask;
+          break;
+        case '2':
+          tileY |= mask;
+          break;
+        case '3':
+          tileX |= mask;
+          tileY |= mask;
+          break;
+        default:
+          throw new IllegalArgumentException("Invalid QuadKey digit: " + 
quadKey);
+      }
+    }
+    return new BingTile(tileX, tileY, zoomLevel);
+  }
+
+  // =========================================================================
+  // Accessors
+  // =========================================================================
+
+  public int getX() {
+    return x;
+  }
+
+  public int getY() {
+    return y;
+  }
+
+  public int getZoomLevel() {
+    return zoomLevel;
+  }
+
+  // =========================================================================
+  // Conversion methods
+  // =========================================================================
+
+  /**
+   * Converts this tile to its quadkey string representation.
+   *
+   * @return the quadkey string
+   */
+  public String toQuadKey() {
+    char[] quadKey = new char[this.zoomLevel];
+    for (int i = this.zoomLevel; i > 0; i--) {
+      char digit = '0';
+      int mask = 1 << (i - 1);
+      if ((this.x & mask) != 0) {
+        digit++;
+      }
+      if ((this.y & mask) != 0) {
+        digit += 2;
+      }
+      quadKey[this.zoomLevel - i] = digit;
+    }
+    return String.valueOf(quadKey);
+  }
+
+  /**
+   * Returns the bounding box envelope of this tile.
+   *
+   * @return the JTS Envelope
+   */
+  public Envelope toEnvelope() {
+    Coordinate upperLeftCorner = tileXYToLatitudeLongitude(this.x, this.y, 
this.zoomLevel);
+    Coordinate lowerRightCorner = tileXYToLatitudeLongitude(this.x + 1, this.y 
+ 1, this.zoomLevel);
+
+    return new Envelope(
+        upperLeftCorner.x, lowerRightCorner.x, lowerRightCorner.y, 
upperLeftCorner.y);
+  }
+
+  /**
+   * Returns the polygon geometry of this tile.
+   *
+   * @return the JTS Polygon
+   */
+  public Polygon toPolygon() {
+    Envelope envelope = toEnvelope();
+    Coordinate[] coordinates =
+        new Coordinate[] {
+          new Coordinate(envelope.getMinX(), envelope.getMinY()),
+          new Coordinate(envelope.getMinX(), envelope.getMaxY()),
+          new Coordinate(envelope.getMaxX(), envelope.getMaxY()),
+          new Coordinate(envelope.getMaxX(), envelope.getMinY()),
+          new Coordinate(envelope.getMinX(), envelope.getMinY())
+        };
+    return GEOMETRY_FACTORY.createPolygon(coordinates);
+  }
+
+  // =========================================================================
+  // Overridden Object methods
+  // =========================================================================
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (other == null || getClass() != other.getClass()) {
+      return false;
+    }
+    BingTile otherTile = (BingTile) other;
+    return this.x == otherTile.x && this.y == otherTile.y && this.zoomLevel == 
otherTile.zoomLevel;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(x, y, zoomLevel);
+  }
+
+  @Override
+  public String toString() {
+    return toQuadKey();
+  }
+
+  // =========================================================================
+  // Spatial query methods
+  // =========================================================================
+
+  /**
+   * Returns the tiles around a given latitude/longitude at the specified zoom 
level. Returns the
+   * 3x3 neighborhood of tiles centered on the tile containing the point.
+   *
+   * @param latitude the latitude
+   * @param longitude the longitude
+   * @param zoomLevel the zoom level
+   * @return list of BingTile objects
+   */
+  public static List<BingTile> tilesAround(double latitude, double longitude, 
int zoomLevel) {
+    checkLatitude(latitude);
+    checkLongitude(longitude);
+    checkZoomLevel(zoomLevel);
+
+    List<BingTile> ret = new ArrayList<>();
+    long mapSize = mapSize(zoomLevel);
+    long maxTileIndex = (mapSize / TILE_PIXELS) - 1;
+
+    int tileX = longitudeToTileX(longitude, mapSize);
+    int tileY = latitudeToTileY(latitude, mapSize);
+
+    for (int i = -1; i <= 1; i++) {
+      for (int j = -1; j <= 1; j++) {
+        int newX = tileX + i;
+        int newY = tileY + j;
+        if (newX >= 0 && newX <= maxTileIndex && newY >= 0 && newY <= 
maxTileIndex) {
+          ret.add(BingTile.fromCoordinates(newX, newY, zoomLevel));
+        }
+      }
+    }
+    return ret;
+  }
+
+  /**
+   * Returns the minimum set of Bing tiles that fully covers a given geometry 
at the specified zoom
+   * level.
+   *
+   * @param geometry the JTS Geometry to cover
+   * @param zoomLevel the zoom level (1 to 23)
+   * @return list of BingTile objects covering the geometry
+   */
+  public static List<BingTile> tilesCovering(Geometry geometry, int zoomLevel) 
{
+    checkZoomLevel(zoomLevel);
+
+    List<BingTile> ret = new ArrayList<>();
+    if (geometry.isEmpty()) {
+      return ret;
+    }
+
+    Envelope envelope = geometry.getEnvelopeInternal();
+
+    checkLatitude(envelope.getMinY());
+    checkLatitude(envelope.getMaxY());
+    checkLongitude(envelope.getMinX());
+    checkLongitude(envelope.getMaxX());
+
+    boolean pointOrRectangle = isPointOrRectangle(geometry, envelope);
+
+    BingTile leftUpperTile = fromLatLon(envelope.getMaxY(), 
envelope.getMinX(), zoomLevel);
+    BingTile rightLowerTile = getTileCoveringLowerRightCorner(envelope, 
zoomLevel);
+
+    // XY coordinates start at (0,0) in the left upper corner and increase 
left to right and
+    // top to bottom
+    long tileCount =
+        (long) (rightLowerTile.getX() - leftUpperTile.getX() + 1)
+            * (rightLowerTile.getY() - leftUpperTile.getY() + 1);
+
+    checkGeometryToBingTilesLimits(geometry, pointOrRectangle, tileCount);
+
+    if (pointOrRectangle || zoomLevel <= OPTIMIZED_TILING_MIN_ZOOM_LEVEL) {
+      // Collect tiles covering the bounding box and check each tile for 
intersection
+      for (int x = leftUpperTile.getX(); x <= rightLowerTile.getX(); x++) {
+        for (int y = leftUpperTile.getY(); y <= rightLowerTile.getY(); y++) {
+          BingTile tile = BingTile.fromCoordinates(x, y, zoomLevel);
+          if (pointOrRectangle || !tileDisjoint(tile, geometry)) {
+            ret.add(tile);
+          }
+        }
+      }
+    } else {
+      // Optimized tiling: identify large tiles fully covered by geometry then 
expand
+      BingTile[] tiles =
+          getTilesInBetween(leftUpperTile, rightLowerTile, 
OPTIMIZED_TILING_MIN_ZOOM_LEVEL);
+      for (BingTile tile : tiles) {
+        appendIntersectingSubtiles(geometry, zoomLevel, tile, ret);
+      }
+    }
+
+    return ret;
+  }
+
+  // =========================================================================
+  // Internal helper methods for tilesCovering
+  // =========================================================================
+
+  private static boolean isPointOrRectangle(Geometry geometry, Envelope 
envelope) {
+    if (geometry instanceof org.locationtech.jts.geom.Point) {
+      return true;
+    }
+    if (!(geometry instanceof Polygon)) {
+      return false;
+    }
+    Polygon polygon = (Polygon) geometry;
+    if (polygon.getNumInteriorRing() > 0) {
+      return false;
+    }
+    Coordinate[] coords = polygon.getExteriorRing().getCoordinates();
+    // A closed rectangle has 5 coordinates (first == last)
+    if (coords.length != 5) {
+      return false;
+    }
+    // Check that all corners match the envelope corners
+    for (int i = 0; i < 4; i++) {
+      Coordinate c = coords[i];
+      boolean matchesCorner =
+          (c.x == envelope.getMinX() || c.x == envelope.getMaxX())
+              && (c.y == envelope.getMinY() || c.y == envelope.getMaxY());
+      if (!matchesCorner) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static void checkGeometryToBingTilesLimits(
+      Geometry geometry, boolean pointOrRectangle, long tileCount) {
+    if (pointOrRectangle) {
+      checkCondition(
+          tileCount <= 1_000_000,
+          "The number of input tiles is too large (more than 1M) to compute a 
set of covering Bing tiles.");
+    } else {
+      checkCondition(
+          (int) tileCount == tileCount,
+          "The zoom level is too high to compute a set of covering Bing 
tiles.");
+
+      long pointCount = geometry.getNumPoints();
+      long complexity;
+      try {
+        complexity = Math.multiplyExact(tileCount, pointCount);
+      } catch (ArithmeticException e) {
+        throw new IllegalArgumentException(
+            "The zoom level is too high or the geometry is too complex to 
compute a set of covering Bing tiles. "
+                + "Please use a lower zoom level or convert the geometry to 
its bounding box using the ST_Envelope function.");
+      }
+      checkCondition(
+          complexity <= 25_000_000,
+          "The zoom level is too high or the geometry is too complex to 
compute a set of covering Bing tiles. "
+              + "Please use a lower zoom level or convert the geometry to its 
bounding box using the ST_Envelope function.");
+    }
+  }
+
+  private static BingTile getTileCoveringLowerRightCorner(Envelope envelope, 
int zoomLevel) {
+    BingTile tile = fromLatLon(envelope.getMinY(), envelope.getMaxX(), 
zoomLevel);
+
+    // If the tile covering the lower right corner of the envelope overlaps 
the envelope only
+    // at the border then return a tile shifted to the left and/or up
+    int deltaX = 0;
+    int deltaY = 0;
+
+    Coordinate upperLeftCorner =
+        tileXYToLatitudeLongitude(tile.getX(), tile.getY(), 
tile.getZoomLevel());
+    if (upperLeftCorner.x == envelope.getMaxX()) {
+      deltaX = -1;
+    }
+    if (upperLeftCorner.y == envelope.getMinY()) {
+      deltaY = -1;
+    }
+
+    if (deltaX == 0 && deltaY == 0) {
+      return tile;
+    } else {
+      return BingTile.fromCoordinates(
+          tile.getX() + deltaX, tile.getY() + deltaY, tile.getZoomLevel());
+    }
+  }
+
+  private static BingTile[] getTilesInBetween(
+      BingTile leftUpperTile, BingTile rightLowerTile, int zoomLevel) {
+    checkCondition(
+        leftUpperTile.getZoomLevel() == rightLowerTile.getZoomLevel(), 
"Mismatched zoom levels");
+    checkCondition(leftUpperTile.getZoomLevel() > zoomLevel, "Tile zoom level 
too low");
+
+    int divisor = 1 << (leftUpperTile.getZoomLevel() - zoomLevel);
+
+    int minX = (int) Math.floor((double) leftUpperTile.getX() / divisor);
+    int maxX = (int) Math.floor((double) rightLowerTile.getX() / divisor);
+    int minY = (int) Math.floor((double) leftUpperTile.getY() / divisor);
+    int maxY = (int) Math.floor((double) rightLowerTile.getY() / divisor);
+
+    BingTile[] tiles = new BingTile[(maxX - minX + 1) * (maxY - minY + 1)];
+
+    int index = 0;
+    for (int x = minX; x <= maxX; x++) {
+      for (int y = minY; y <= maxY; y++) {
+        tiles[index] = BingTile.fromCoordinates(x, y, 
OPTIMIZED_TILING_MIN_ZOOM_LEVEL);
+        index++;
+      }
+    }
+    return tiles;
+  }
+
+  private static boolean tileDisjoint(BingTile tile, Geometry geometry) {
+    Polygon tilePolygon = tile.toPolygon();
+    return tilePolygon.disjoint(geometry);
+  }
+
+  private static boolean geometryContainsTile(Geometry geometry, BingTile 
tile) {
+    Polygon tilePolygon = tile.toPolygon();
+    return geometry.contains(tilePolygon);
+  }
+
+  private static void appendIntersectingSubtiles(
+      Geometry geometry, int zoomLevel, BingTile tile, List<BingTile> result) {
+    int tileZoomLevel = tile.getZoomLevel();
+    checkCondition(tileZoomLevel <= zoomLevel, "Tile zoom level too high");
+
+    if (tileZoomLevel == zoomLevel) {
+      if (!tileDisjoint(tile, geometry)) {
+        result.add(tile);
+      }
+      return;
+    }
+
+    if (geometryContainsTile(geometry, tile)) {
+      // Tile is fully contained — add all sub-tiles at the target zoom level
+      int subTileCount = 1 << (zoomLevel - tileZoomLevel);
+      int minX = subTileCount * tile.getX();
+      int minY = subTileCount * tile.getY();
+
+      for (int x = minX; x < minX + subTileCount; x++) {
+        for (int y = minY; y < minY + subTileCount; y++) {
+          result.add(BingTile.fromCoordinates(x, y, zoomLevel));
+        }
+      }
+      return;
+    }
+
+    if (tileDisjoint(tile, geometry)) {
+      return;
+    }
+
+    // Recurse into the 4 children
+    int nextZoomLevel = tileZoomLevel + 1;
+    int minX = 2 * tile.getX();
+    int minY = 2 * tile.getY();
+
+    for (int x = minX; x < minX + 2; x++) {
+      for (int y = minY; y < minY + 2; y++) {
+        appendIntersectingSubtiles(
+            geometry, zoomLevel, BingTile.fromCoordinates(x, y, 
nextZoomLevel), result);
+      }
+    }
+  }
+}
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 61fd47ae34..6d9e04b65f 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -5387,4 +5387,315 @@ public class FunctionsTest extends TestBase {
     // 13 chars * 5 bits = 65 > 64 (MAX_BIT_PRECISION)
     Functions.geohashNeighbors("0123456789abc");
   }
+
+  // =========================================================================
+  // Bing Tile function tests
+  // =========================================================================
+
+  // --- ST_BingTile (from coordinates and from quadkey) ---
+
+  @Test
+  public void testBingTileFromCoordinates() {
+    // bing_tile(3, 5, 3) = quadkey "213"
+    assertEquals("213", Functions.bingTile(3, 5, 3));
+    // bing_tile(21845, 13506, 15) = quadkey "123030123010121"
+    assertEquals("123030123010121", Functions.bingTile(21845, 13506, 15));
+  }
+
+  @Test
+  public void testBingTileQuadKeyRoundTrip() {
+    assertEquals("213", Functions.bingTileQuadKey(Functions.bingTile(3, 5, 
3)));
+    assertEquals(
+        "123030123010121", Functions.bingTileQuadKey(Functions.bingTile(21845, 
13506, 15)));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBingTileInvalidEmptyQuadKey() {
+    Functions.bingTileQuadKey("");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBingTileInvalidQuadKeyDigits() {
+    Functions.bingTileQuadKey("test");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBingTileInvalidQuadKeyDigits2() {
+    // "12345" contains '4' and '5' which are invalid
+    Functions.bingTileQuadKey("12345");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBingTileQuadKeyTooLong() {
+    // quadkey > 23 chars
+    
Functions.bingTileQuadKey("101010101010101010101010101010100101010101001010");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBingTileXYOutOfRange() {
+    // x=10 at zoom 3 is out of [0, 8) range
+    Functions.bingTile(10, 2, 3);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBingTileXYOutOfRange2() {
+    // y=10 at zoom 3 is out of [0, 8) range
+    Functions.bingTile(2, 10, 3);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBingTileZoomOutOfRange() {
+    // zoom=37 > 23
+    Functions.bingTile(2, 7, 37);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBingTileZoomZero() {
+    // zoom=0 is invalid
+    Functions.bingTile(0, 0, 0);
+  }
+
+  // --- ST_BingTileAt (point to tile) ---
+
+  @Test
+  public void testBingTileAt() {
+    // bingTileAt(lon=60, lat=30.12, zoom=15) = tile(21845, 13506, 15)
+    String qk = Functions.bingTileAt(60, 30.12, 15);
+    assertEquals("123030123010121", qk);
+    assertEquals(21845, Functions.bingTileX(qk));
+    assertEquals(13506, Functions.bingTileY(qk));
+
+    // bingTileAt(lon=-0.002, lat=0, zoom=1) = tile(0, 1, 1)
+    qk = Functions.bingTileAt(-0.002, 0, 1);
+    assertEquals(0, Functions.bingTileX(qk));
+    assertEquals(1, Functions.bingTileY(qk));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBingTileAtLongitudeOutOfRange() {
+    // longitude 600 is out of range
+    Functions.bingTileAt(600, 30.12, 15);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBingTileAtLatitudeOutOfRange() {
+    // latitude 300.12 is out of range
+    Functions.bingTileAt(60, 300.12, 15);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBingTileAtZoomTooSmall() {
+    // zoom=0
+    Functions.bingTileAt(60, 30.12, 0);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBingTileAtZoomTooLarge() {
+    // zoom=40
+    Functions.bingTileAt(60, 30.12, 40);
+  }
+
+  // --- ST_BingTileCoordinates (X, Y) ---
+
+  @Test
+  public void testBingTileCoordinates() {
+    // bing_tile_coordinates('213') = (3, 5)
+    assertEquals(3, Functions.bingTileX("213"));
+    assertEquals(5, Functions.bingTileY("213"));
+
+    // bing_tile_coordinates('123030123010121') = (21845, 13506)
+    assertEquals(21845, Functions.bingTileX("123030123010121"));
+    assertEquals(13506, Functions.bingTileY("123030123010121"));
+  }
+
+  // --- ST_BingTileZoomLevel ---
+
+  @Test
+  public void testBingTileZoomLevel() {
+    assertEquals(3, Functions.bingTileZoomLevel("213"));
+    assertEquals(15, Functions.bingTileZoomLevel("123030123010121"));
+  }
+
+  // --- ST_BingTilesAround ---
+
+  @Test
+  public void testBingTilesAround() {
+    // bingTilesAround(lon=60, lat=30.12, zoom=1) = ["0", "2", "1", "3"]
+    String[] tiles = Functions.bingTilesAround(60, 30.12, 1);
+    assertEquals(4, tiles.length);
+    assertArrayEquals(new String[] {"0", "2", "1", "3"}, tiles);
+
+    // bingTilesAround(lon=60, lat=30.12, zoom=15) = 9 tiles (3x3 neighborhood)
+    tiles = Functions.bingTilesAround(60, 30.12, 15);
+    assertEquals(9, tiles.length);
+    assertArrayEquals(
+        new String[] {
+          "123030123010102",
+          "123030123010120",
+          "123030123010122",
+          "123030123010103",
+          "123030123010121",
+          "123030123010123",
+          "123030123010112",
+          "123030123010130",
+          "123030123010132"
+        },
+        tiles);
+  }
+
+  @Test
+  public void testBingTilesAroundCorner() {
+    // corner case at (lon=-180, lat=-85.05112878, zoom=1) = all 4 tiles
+    String[] tiles = Functions.bingTilesAround(-180, -85.05112878, 1);
+    assertArrayEquals(new String[] {"0", "2", "1", "3"}, tiles);
+
+    // corner at (lon=-180, lat=-85.05112878, zoom=3) = 4 tiles
+    tiles = Functions.bingTilesAround(-180, -85.05112878, 3);
+    assertArrayEquals(new String[] {"220", "222", "221", "223"}, tiles);
+  }
+
+  @Test
+  public void testBingTilesAroundEdge() {
+    // edge case at (lon=0, lat=-85.05112878, zoom=2) = 6 tiles
+    String[] tiles = Functions.bingTilesAround(0, -85.05112878, 2);
+    assertArrayEquals(new String[] {"21", "23", "30", "32", "31", "33"}, 
tiles);
+
+    // edge at (lon=0, lat=85.05112878, zoom=2) = 6 tiles
+    tiles = Functions.bingTilesAround(0, 85.05112878, 2);
+    assertArrayEquals(new String[] {"01", "03", "10", "12", "11", "13"}, 
tiles);
+  }
+
+  // --- ST_BingTilePolygon ---
+
+  @Test
+  public void testBingTilePolygon() {
+    // bing_tile_polygon('123030123010121')
+    Geometry polygon = Functions.bingTilePolygon("123030123010121");
+    assertNotNull(polygon);
+    assertTrue(polygon instanceof Polygon);
+    assertEquals(5, polygon.getCoordinates().length);
+
+    // Expected envelope for tile '123030123010121'
+    Envelope env = polygon.getEnvelopeInternal();
+    assertEquals(59.996337890625, env.getMinX(), 1e-10);
+    assertEquals(60.00732421875, env.getMaxX(), 1e-10);
+    assertEquals(30.11662158281937, env.getMinY(), 1e-10);
+    assertEquals(30.12612436422458, env.getMaxY(), 1e-10);
+  }
+
+  @Test
+  public void testBingTilePolygonBottomRightCorner() {
+    // bottom-right corner of tile(1,1,1) = (180, -85.05112877980659)
+    Geometry polygon = Functions.bingTilePolygon(Functions.bingTile(1, 1, 1));
+    Envelope env = polygon.getEnvelopeInternal();
+    assertEquals(180.0, env.getMaxX(), 1e-10);
+    assertEquals(-85.05112877980659, env.getMinY(), 1e-10);
+
+    // bottom-right corner of tile(3,3,2) = (180, -85.05112877980659)
+    polygon = Functions.bingTilePolygon(Functions.bingTile(3, 3, 2));
+    env = polygon.getEnvelopeInternal();
+    assertEquals(180.0, env.getMaxX(), 1e-10);
+    assertEquals(-85.05112877980659, env.getMinY(), 1e-10);
+  }
+
+  @Test
+  public void testBingTilePolygonOriginCorner() {
+    // top-left corner of tile(0,0,1) bottom-right = (0, 0)
+    Geometry polygon = Functions.bingTilePolygon(Functions.bingTile(0, 0, 1));
+    Envelope env = polygon.getEnvelopeInternal();
+    assertEquals(0.0, env.getMaxX(), 1e-10);
+    assertEquals(0.0, env.getMinY(), 1e-10);
+
+    // top-left corner of tile(0,0,1) = (-180, 85.05112877980659)
+    assertEquals(-180.0, env.getMinX(), 1e-10);
+    assertEquals(85.05112877980659, env.getMaxY(), 1e-10);
+  }
+
+  // --- ST_BingTileCellIDs (geometry_to_bing_tiles) ---
+
+  @Test
+  public void testBingTileCellIDsPoint() {
+    // geometry_to_bing_tiles(POINT(60 30.12), 10) = ["1230301230"]
+    Geometry point = GEOMETRY_FACTORY.createPoint(new Coordinate(60, 30.12));
+    String[] tiles = Functions.bingTileCellIDs(point, 10);
+    assertArrayEquals(new String[] {"1230301230"}, tiles);
+
+    // geometry_to_bing_tiles(POINT(60 30.12), 15) = ["123030123010121"]
+    tiles = Functions.bingTileCellIDs(point, 15);
+    assertArrayEquals(new String[] {"123030123010121"}, tiles);
+
+    // geometry_to_bing_tiles(POINT(60 30.12), 16) = ["1230301230101212"]
+    tiles = Functions.bingTileCellIDs(point, 16);
+    assertArrayEquals(new String[] {"1230301230101212"}, tiles);
+  }
+
+  @Test
+  public void testBingTileCellIDsPolygon() {
+    // geometry_to_bing_tiles(POLYGON((0 0, 0 10, 10 10, 10 0)), 6)
+    Geometry polygon =
+        GEOMETRY_FACTORY.createPolygon(
+            coordArray(0.0, 0.0, 0.0, 10.0, 10.0, 10.0, 10.0, 0.0, 0.0, 0.0));
+    String[] tiles = Functions.bingTileCellIDs(polygon, 6);
+    assertArrayEquals(new String[] {"122220", "122222", "122221", "122223"}, 
tiles);
+  }
+
+  @Test
+  public void testBingTileCellIDsTriangle() {
+    // geometry_to_bing_tiles(POLYGON((10 10, -10 10, -20 -15, 10 10)), 3)
+    Geometry triangle =
+        GEOMETRY_FACTORY.createPolygon(
+            coordArray(10.0, 10.0, -10.0, 10.0, -20.0, -15.0, 10.0, 10.0));
+    String[] tiles = Functions.bingTileCellIDs(triangle, 3);
+    assertArrayEquals(new String[] {"033", "211", "122"}, tiles);
+  }
+
+  @Test
+  public void testBingTileCellIDsEmpty() {
+    // POINT EMPTY, POLYGON EMPTY → empty list
+    Geometry emptyPoint = GEOMETRY_FACTORY.createPoint();
+    assertEquals(0, Functions.bingTileCellIDs(emptyPoint, 10).length);
+
+    Geometry emptyPolygon = GEOMETRY_FACTORY.createPolygon();
+    assertEquals(0, Functions.bingTileCellIDs(emptyPolygon, 10).length);
+  }
+
+  @Test
+  public void testBingTileCellIDsSelfRoundTrip() {
+    // geometry_to_bing_tiles(bing_tile_polygon('1230301230'), 10) = 
["1230301230"]
+    Geometry tilePolygon = Functions.bingTilePolygon("1230301230");
+    String[] tiles = Functions.bingTileCellIDs(tilePolygon, 10);
+    assertArrayEquals(new String[] {"1230301230"}, tiles);
+
+    // expanding to zoom 11 = 4 child tiles
+    tiles = Functions.bingTileCellIDs(tilePolygon, 11);
+    assertArrayEquals(
+        new String[] {"12303012300", "12303012302", "12303012301", 
"12303012303"}, tiles);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBingTileCellIDsZoomTooSmall() {
+    // zoom=0 is invalid
+    Geometry point = GEOMETRY_FACTORY.createPoint(new Coordinate(60, 30.12));
+    Functions.bingTileCellIDs(point, 0);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testBingTileCellIDsZoomTooLarge() {
+    // zoom=40 is invalid
+    Geometry point = GEOMETRY_FACTORY.createPoint(new Coordinate(60, 30.12));
+    Functions.bingTileCellIDs(point, 40);
+  }
+
+  // --- ST_BingTileToGeom ---
+
+  @Test
+  public void testBingTileToGeom() {
+    String[] quadkeys = new String[] {"0", "1", "2", "3"};
+    Geometry[] polygons = Functions.bingTileToGeom(quadkeys);
+    assertEquals(4, polygons.length);
+    for (Geometry g : polygons) {
+      assertTrue(g instanceof Polygon);
+      assertEquals(5, g.getCoordinates().length);
+    }
+  }
 }
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index 58871582a1..180ebccc21 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -663,6 +663,189 @@ Output:
 32618
 ```
 
+## ST_BingTile
+
+Introduction: Creates a Bing Tile quadkey from tile XY coordinates and a zoom 
level.
+
+Format: `ST_BingTile(tileX: Integer, tileY: Integer, zoomLevel: Integer)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTile(3, 5, 3)
+```
+
+Output:
+
+```
+213
+```
+
+## ST_BingTileAt
+
+Introduction: Returns the Bing Tile quadkey for a given point (longitude, 
latitude) at a specified zoom level.
+
+Format: `ST_BingTileAt(longitude: Double, latitude: Double, zoomLevel: 
Integer)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTileAt(60, 30.12, 15)
+```
+
+Output:
+
+```
+123030123010121
+```
+
+## ST_BingTileCellIDs
+
+Introduction: Returns an array of Bing Tile quadkey strings that cover the 
given geometry at the specified zoom level.
+
+Format: `ST_BingTileCellIDs(geom: Geometry, zoomLevel: Integer)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTileCellIDs(ST_GeomFromText('POINT(60 30.12)'), 10)
+```
+
+Output:
+
+```
+[1230301230]
+```
+
+## ST_BingTilePolygon
+
+Introduction: Returns the bounding polygon (Geometry) of the Bing Tile 
identified by the given quadkey.
+
+Format: `ST_BingTilePolygon(quadKey: String)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_AsText(ST_BingTilePolygon('213'))
+```
+
+Output:
+
+```
+POLYGON ((0 0, 0 -40.97989806962013, 45 -40.97989806962013, 45 0, 0 0))
+```
+
+## ST_BingTilesAround
+
+Introduction: Returns an array of Bing Tile quadkey strings representing the 
neighborhood tiles around the tile that contains the given point (longitude, 
latitude) at the specified zoom level. Returns the 3×3 neighborhood (up to 9 
tiles), or fewer tiles at the edges/corners of the map.
+
+Format: `ST_BingTilesAround(longitude: Double, latitude: Double, zoomLevel: 
Integer)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTilesAround(60, 30.12, 1)
+```
+
+Output:
+
+```
+[0, 2, 1, 3]
+```
+
+## ST_BingTileToGeom
+
+Introduction: Returns an array of Polygons for the corresponding Bing Tile 
quadkeys.
+
+!!!Hint
+    To convert a Polygon array to a single geometry, use 
[ST_Collect](#st_collect).
+
+Format: `ST_BingTileToGeom(quadKeys: Array[String])`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTileToGeom(array('0', '1', '2', '3'))
+```
+
+Output:
+
+```
+[POLYGON ((-180 85.05112877980659, -180 0, 0 0, 0 85.05112877980659, -180 
85.05112877980659)), ...]
+```
+
+## ST_BingTileX
+
+Introduction: Returns the tile X coordinate of the Bing Tile identified by the 
given quadkey.
+
+Format: `ST_BingTileX(quadKey: String)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTileX('213')
+```
+
+Output:
+
+```
+3
+```
+
+## ST_BingTileY
+
+Introduction: Returns the tile Y coordinate of the Bing Tile identified by the 
given quadkey.
+
+Format: `ST_BingTileY(quadKey: String)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTileY('213')
+```
+
+Output:
+
+```
+5
+```
+
+## ST_BingTileZoomLevel
+
+Introduction: Returns the zoom level of the Bing Tile identified by the given 
quadkey.
+
+Format: `ST_BingTileZoomLevel(quadKey: String)`
+
+Since: `v1.9.0`
+
+Example:
+
+```sql
+SELECT ST_BingTileZoomLevel('213')
+```
+
+Output:
+
+```
+3
+```
+
 ## ST_Boundary
 
 Introduction: Returns the closure of the combinatorial boundary of this 
Geometry.
diff --git a/docs/api/snowflake/vector-data/Function.md 
b/docs/api/snowflake/vector-data/Function.md
index fdb09919fb..185e009901 100644
--- a/docs/api/snowflake/vector-data/Function.md
+++ b/docs/api/snowflake/vector-data/Function.md
@@ -497,6 +497,168 @@ Output:
 32618
 ```
 
+## ST_BingTile
+
+Introduction: Creates a Bing Tile quadkey from tile XY coordinates and a zoom 
level.
+
+Format: `ST_BingTile(tileX: Int, tileY: Int, zoomLevel: Int)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTile(3, 5, 3)
+```
+
+Output:
+
+```
+213
+```
+
+## ST_BingTileAt
+
+Introduction: Returns the Bing Tile quadkey for a given point (longitude, 
latitude) at a specified zoom level.
+
+Format: `ST_BingTileAt(longitude: Double, latitude: Double, zoomLevel: Int)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTileAt(60, 30.12, 15)
+```
+
+Output:
+
+```
+123030123010121
+```
+
+## ST_BingTileCellIDs
+
+Introduction: Returns an array of Bing Tile quadkey strings that cover the 
given geometry at the specified zoom level.
+
+Format: `ST_BingTileCellIDs(geom: geometry, zoomLevel: Int)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTileCellIDs(ST_GeomFromText('POINT(60 30.12)'), 10)
+```
+
+Output:
+
+```
+[1230301230]
+```
+
+## ST_BingTilePolygon
+
+Introduction: Returns the bounding polygon (Geometry) of the Bing Tile 
identified by the given quadkey.
+
+Format: `ST_BingTilePolygon(quadKey: String)`
+
+SQL example:
+
+```sql
+SELECT ST_AsText(ST_BingTilePolygon('213'))
+```
+
+Output:
+
+```
+POLYGON ((0 0, 0 -40.97989806962013, 45 -40.97989806962013, 45 0, 0 0))
+```
+
+## ST_BingTilesAround
+
+Introduction: Returns an array of Bing Tile quadkey strings representing the 
neighborhood tiles around the tile that contains the given point (longitude, 
latitude) at the specified zoom level. Returns the 3×3 neighborhood (up to 9 
tiles), or fewer tiles at the edges/corners of the map.
+
+Format: `ST_BingTilesAround(longitude: Double, latitude: Double, zoomLevel: 
Int)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTilesAround(60, 30.12, 1)
+```
+
+Output:
+
+```
+[0, 2, 1, 3]
+```
+
+## ST_BingTileToGeom
+
+Introduction: Returns a GeometryCollection of Polygons for the corresponding 
Bing Tile quadkeys.
+
+Format: `ST_BingTileToGeom(quadKeys: Array)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTileToGeom(ARRAY_CONSTRUCT('0', '1', '2', '3'))
+```
+
+Output:
+
+```
+GEOMETRYCOLLECTION (POLYGON ((-180 85.05112877980659, -180 0, 0 0, 0 
85.05112877980659, -180 85.05112877980659)), ...)
+```
+
+## ST_BingTileX
+
+Introduction: Returns the tile X coordinate of the Bing Tile identified by the 
given quadkey.
+
+Format: `ST_BingTileX(quadKey: String)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTileX('213')
+```
+
+Output:
+
+```
+3
+```
+
+## ST_BingTileY
+
+Introduction: Returns the tile Y coordinate of the Bing Tile identified by the 
given quadkey.
+
+Format: `ST_BingTileY(quadKey: String)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTileY('213')
+```
+
+Output:
+
+```
+5
+```
+
+## ST_BingTileZoomLevel
+
+Introduction: Returns the zoom level of the Bing Tile identified by the given 
quadkey.
+
+Format: `ST_BingTileZoomLevel(quadKey: String)`
+
+SQL example:
+
+```sql
+SELECT ST_BingTileZoomLevel('213')
+```
+
+Output:
+
+```
+3
+```
+
 ## ST_Boundary
 
 Introduction: Returns the closure of the combinatorial boundary of this 
Geometry.
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index 31a8f3d682..7afea7b69d 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -736,6 +736,189 @@ Output:
 32618
 ```
 
+## ST_BingTile
+
+Introduction: Creates a Bing Tile quadkey from tile XY coordinates and a zoom 
level.
+
+Format: `ST_BingTile(tileX: Integer, tileY: Integer, zoomLevel: Integer)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTile(3, 5, 3)
+```
+
+Output:
+
+```
+213
+```
+
+## ST_BingTileAt
+
+Introduction: Returns the Bing Tile quadkey for a given point (longitude, 
latitude) at a specified zoom level.
+
+Format: `ST_BingTileAt(longitude: Double, latitude: Double, zoomLevel: 
Integer)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTileAt(60, 30.12, 15)
+```
+
+Output:
+
+```
+123030123010121
+```
+
+## ST_BingTileCellIDs
+
+Introduction: Returns an array of Bing Tile quadkey strings that cover the 
given geometry at the specified zoom level.
+
+Format: `ST_BingTileCellIDs(geom: Geometry, zoomLevel: Integer)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTileCellIDs(ST_GeomFromText('POINT(60 30.12)'), 10)
+```
+
+Output:
+
+```
+[1230301230]
+```
+
+## ST_BingTilePolygon
+
+Introduction: Returns the bounding polygon (Geometry) of the Bing Tile 
identified by the given quadkey.
+
+Format: `ST_BingTilePolygon(quadKey: String)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_BingTilePolygon('213'))
+```
+
+Output:
+
+```
+POLYGON ((0 0, 0 -40.97989806962013, 45 -40.97989806962013, 45 0, 0 0))
+```
+
+## ST_BingTilesAround
+
+Introduction: Returns an array of Bing Tile quadkey strings representing the 
neighborhood tiles around the tile that contains the given point (longitude, 
latitude) at the specified zoom level. Returns the 3×3 neighborhood (up to 9 
tiles), or fewer tiles at the edges/corners of the map.
+
+Format: `ST_BingTilesAround(longitude: Double, latitude: Double, zoomLevel: 
Integer)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTilesAround(60, 30.12, 1)
+```
+
+Output:
+
+```
+[0, 2, 1, 3]
+```
+
+## ST_BingTileToGeom
+
+Introduction: Returns an array of Polygons for the corresponding Bing Tile 
quadkeys.
+
+!!!Hint
+    To convert a Polygon array to a single geometry, use 
[ST_Collect](#st_collect).
+
+Format: `ST_BingTileToGeom(quadKeys: Array[String])`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTileToGeom(array('0', '1', '2', '3'))
+```
+
+Output:
+
+```
+[POLYGON ((-180 85.05112877980659, -180 0, 0 0, 0 85.05112877980659, -180 
85.05112877980659)), ...]
+```
+
+## ST_BingTileX
+
+Introduction: Returns the tile X coordinate of the Bing Tile identified by the 
given quadkey.
+
+Format: `ST_BingTileX(quadKey: String)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTileX('213')
+```
+
+Output:
+
+```
+3
+```
+
+## ST_BingTileY
+
+Introduction: Returns the tile Y coordinate of the Bing Tile identified by the 
given quadkey.
+
+Format: `ST_BingTileY(quadKey: String)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTileY('213')
+```
+
+Output:
+
+```
+5
+```
+
+## ST_BingTileZoomLevel
+
+Introduction: Returns the zoom level of the Bing Tile identified by the given 
quadkey.
+
+Format: `ST_BingTileZoomLevel(quadKey: String)`
+
+Since: `v1.9.0`
+
+SQL Example
+
+```sql
+SELECT ST_BingTileZoomLevel('213')
+```
+
+Output:
+
+```
+3
+```
+
 ## ST_BinaryDistanceBandColumn
 
 Introduction: Introduction: Returns a `weights` column containing every record 
in a dataframe within a specified `threshold` distance.
diff --git a/flink/src/main/java/org/apache/sedona/flink/Catalog.java 
b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
index 5a02e2efdb..2d3646c43c 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -88,6 +88,15 @@ public class Catalog {
       new Functions.ST_H3CellIDs(),
       new Functions.ST_H3KRing(),
       new Functions.ST_H3ToGeom(),
+      new Functions.ST_BingTile(),
+      new Functions.ST_BingTileAt(),
+      new Functions.ST_BingTilesAround(),
+      new Functions.ST_BingTileZoomLevel(),
+      new Functions.ST_BingTileX(),
+      new Functions.ST_BingTileY(),
+      new Functions.ST_BingTilePolygon(),
+      new Functions.ST_BingTileCellIDs(),
+      new Functions.ST_BingTileToGeom(),
       new Functions.ST_Dump(),
       new Functions.ST_DumpPoints(),
       new Functions.ST_DelaunayTriangles(),
diff --git 
a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java 
b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
index c4567a6a98..c5610fe1d6 100644
--- a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
+++ b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
@@ -2338,6 +2338,95 @@ public class Functions {
     }
   }
 
+  // =========================================================================
+  // Bing Tile functions
+  // =========================================================================
+
+  public static class ST_BingTile extends ScalarFunction {
+    @DataTypeHint("STRING")
+    public String eval(
+        @DataTypeHint("INT") Integer tileX,
+        @DataTypeHint("INT") Integer tileY,
+        @DataTypeHint("INT") Integer zoomLevel) {
+      return org.apache.sedona.common.Functions.bingTile(tileX, tileY, 
zoomLevel);
+    }
+  }
+
+  public static class ST_BingTileAt extends ScalarFunction {
+    @DataTypeHint("STRING")
+    public String eval(
+        @DataTypeHint("DOUBLE") Double longitude,
+        @DataTypeHint("DOUBLE") Double latitude,
+        @DataTypeHint("INT") Integer zoomLevel) {
+      return org.apache.sedona.common.Functions.bingTileAt(longitude, 
latitude, zoomLevel);
+    }
+  }
+
+  public static class ST_BingTilesAround extends ScalarFunction {
+    @DataTypeHint("ARRAY<STRING>")
+    public String[] eval(
+        @DataTypeHint("DOUBLE") Double longitude,
+        @DataTypeHint("DOUBLE") Double latitude,
+        @DataTypeHint("INT") Integer zoomLevel) {
+      return org.apache.sedona.common.Functions.bingTilesAround(longitude, 
latitude, zoomLevel);
+    }
+  }
+
+  public static class ST_BingTileZoomLevel extends ScalarFunction {
+    @DataTypeHint("INT")
+    public Integer eval(@DataTypeHint("STRING") String quadKey) {
+      return org.apache.sedona.common.Functions.bingTileZoomLevel(quadKey);
+    }
+  }
+
+  public static class ST_BingTileX extends ScalarFunction {
+    @DataTypeHint("INT")
+    public Integer eval(@DataTypeHint("STRING") String quadKey) {
+      return org.apache.sedona.common.Functions.bingTileX(quadKey);
+    }
+  }
+
+  public static class ST_BingTileY extends ScalarFunction {
+    @DataTypeHint("INT")
+    public Integer eval(@DataTypeHint("STRING") String quadKey) {
+      return org.apache.sedona.common.Functions.bingTileY(quadKey);
+    }
+  }
+
+  public static class ST_BingTilePolygon extends ScalarFunction {
+    @DataTypeHint(
+        value = "RAW",
+        rawSerializer = GeometryTypeSerializer.class,
+        bridgedTo = Geometry.class)
+    public Geometry eval(@DataTypeHint("STRING") String quadKey) {
+      return org.apache.sedona.common.Functions.bingTilePolygon(quadKey);
+    }
+  }
+
+  public static class ST_BingTileCellIDs extends ScalarFunction {
+    @DataTypeHint("ARRAY<STRING>")
+    public String[] eval(
+        @DataTypeHint(
+                value = "RAW",
+                rawSerializer = GeometryTypeSerializer.class,
+                bridgedTo = Geometry.class)
+            Object o,
+        @DataTypeHint("INT") Integer zoomLevel) {
+      Geometry geom = (Geometry) o;
+      return org.apache.sedona.common.Functions.bingTileCellIDs(geom, 
zoomLevel);
+    }
+  }
+
+  public static class ST_BingTileToGeom extends ScalarFunction {
+    @DataTypeHint(
+        value = "RAW",
+        rawSerializer = GeometryArrayTypeSerializer.class,
+        bridgedTo = Geometry[].class)
+    public Geometry[] eval(@DataTypeHint("ARRAY<STRING>") String[] quadKeys) {
+      return org.apache.sedona.common.Functions.bingTileToGeom(quadKeys);
+    }
+  }
+
   public static class ST_Simplify extends ScalarFunction {
     @DataTypeHint(
         value = "RAW",
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 3c9b271941..a348c074d7 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -3077,4 +3077,182 @@ public class FunctionTest extends TestBase {
                 .getField(0);
     assertEquals("ST_MultiLineString", actual);
   }
+
+  // =========================================================================
+  // Bing Tile function tests
+  // =========================================================================
+
+  @Test
+  public void testBingTile() {
+    // bing_tile(3, 5, 3) = "213"
+    String result = (String) first(tableEnv.sqlQuery("SELECT ST_BingTile(3, 5, 
3)")).getField(0);
+    assertEquals("213", result);
+
+    // bing_tile(21845, 13506, 15) = "123030123010121"
+    result = (String) first(tableEnv.sqlQuery("SELECT ST_BingTile(21845, 
13506, 15)")).getField(0);
+    assertEquals("123030123010121", result);
+  }
+
+  @Test
+  public void testBingTileAt() {
+    // ST_BingTileAt(lon, lat, zoom) — longitude first
+    // bingTileAt(lon=60, lat=30.12, zoom=15) = tile(21845, 13506, 15) = 
"123030123010121"
+    String result =
+        (String) first(tableEnv.sqlQuery("SELECT ST_BingTileAt(60.0, 30.12, 
15)")).getField(0);
+    assertEquals("123030123010121", result);
+
+    // bingTileAt(lon=-0.002, lat=0, zoom=1) = tile(0, 1, 1)
+    result = (String) first(tableEnv.sqlQuery("SELECT ST_BingTileAt(-0.002, 
0.0, 1)")).getField(0);
+    int x = (int) first(tableEnv.sqlQuery("SELECT ST_BingTileX('" + result + 
"')")).getField(0);
+    int y = (int) first(tableEnv.sqlQuery("SELECT ST_BingTileY('" + result + 
"')")).getField(0);
+    assertEquals(0, x);
+    assertEquals(1, y);
+  }
+
+  @Test
+  public void testBingTilesAround() {
+    // ST_BingTilesAround(lon, lat, zoom) — longitude first
+    // bingTilesAround(lon=60, lat=30.12, zoom=1) = ["0", "2", "1", "3"]
+    String[] result =
+        (String[])
+            first(tableEnv.sqlQuery("SELECT ST_BingTilesAround(60.0, 30.12, 
1)")).getField(0);
+    assertArrayEquals(new String[] {"0", "2", "1", "3"}, result);
+
+    // bingTilesAround(lon=60, lat=30.12, zoom=15) = 9 tiles
+    result =
+        (String[])
+            first(tableEnv.sqlQuery("SELECT ST_BingTilesAround(60.0, 30.12, 
15)")).getField(0);
+    assertArrayEquals(
+        new String[] {
+          "123030123010102",
+          "123030123010120",
+          "123030123010122",
+          "123030123010103",
+          "123030123010121",
+          "123030123010123",
+          "123030123010112",
+          "123030123010130",
+          "123030123010132"
+        },
+        result);
+
+    // corner at (lon=-180, lat=-85.05112878, zoom=3) = 4 tiles
+    result =
+        (String[])
+            first(tableEnv.sqlQuery("SELECT ST_BingTilesAround(-180.0, 
-85.05112878, 3)"))
+                .getField(0);
+    assertArrayEquals(new String[] {"220", "222", "221", "223"}, result);
+
+    // edge at (lon=0, lat=-85.05112878, zoom=2) = 6 tiles
+    result =
+        (String[])
+            first(tableEnv.sqlQuery("SELECT ST_BingTilesAround(0.0, 
-85.05112878, 2)")).getField(0);
+    assertArrayEquals(new String[] {"21", "23", "30", "32", "31", "33"}, 
result);
+  }
+
+  @Test
+  public void testBingTileZoomLevel() {
+    int result = (int) first(tableEnv.sqlQuery("SELECT 
ST_BingTileZoomLevel('213')")).getField(0);
+    assertEquals(3, result);
+
+    result =
+        (int)
+            first(tableEnv.sqlQuery("SELECT 
ST_BingTileZoomLevel('123030123010121')")).getField(0);
+    assertEquals(15, result);
+  }
+
+  @Test
+  public void testBingTileXY() {
+    int x = (int) first(tableEnv.sqlQuery("SELECT 
ST_BingTileX('213')")).getField(0);
+    int y = (int) first(tableEnv.sqlQuery("SELECT 
ST_BingTileY('213')")).getField(0);
+    assertEquals(3, x);
+    assertEquals(5, y);
+
+    x = (int) first(tableEnv.sqlQuery("SELECT 
ST_BingTileX('123030123010121')")).getField(0);
+    y = (int) first(tableEnv.sqlQuery("SELECT 
ST_BingTileY('123030123010121')")).getField(0);
+    assertEquals(21845, x);
+    assertEquals(13506, y);
+
+    // round-trip bing_tile(x, y, zoom) = original quadkey
+    String reconstructed =
+        (String)
+            first(
+                    tableEnv.sqlQuery(
+                        "SELECT ST_BingTile(ST_BingTileX('123030123010121'), 
ST_BingTileY('123030123010121'), ST_BingTileZoomLevel('123030123010121'))"))
+                .getField(0);
+    assertEquals("123030123010121", reconstructed);
+  }
+
+  @Test
+  public void testBingTilePolygon() {
+    Geometry result =
+        (Geometry)
+            first(tableEnv.sqlQuery("SELECT 
ST_BingTilePolygon('123030123010121')")).getField(0);
+    assertNotNull(result);
+    assertTrue(result instanceof Polygon);
+    Envelope env = result.getEnvelopeInternal();
+    assertEquals(59.996337890625, env.getMinX(), 1e-10);
+    assertEquals(60.00732421875, env.getMaxX(), 1e-10);
+    assertEquals(30.11662158281937, env.getMinY(), 1e-10);
+    assertEquals(30.12612436422458, env.getMaxY(), 1e-10);
+  }
+
+  @Test
+  public void testBingTileCellIDs() {
+    // geometry_to_bing_tiles(POINT(60 30.12), 10) = ["1230301230"]
+    String[] result =
+        (String[])
+            first(
+                    tableEnv.sqlQuery(
+                        "SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POINT (60 
30.12)'), 10)"))
+                .getField(0);
+    assertArrayEquals(new String[] {"1230301230"}, result);
+
+    // geometry_to_bing_tiles(POINT(60 30.12), 15) = ["123030123010121"]
+    result =
+        (String[])
+            first(
+                    tableEnv.sqlQuery(
+                        "SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POINT (60 
30.12)'), 15)"))
+                .getField(0);
+    assertArrayEquals(new String[] {"123030123010121"}, result);
+
+    // geometry_to_bing_tiles(POLYGON((0 0, 0 10, 10 10, 10 0)), 6)
+    result =
+        (String[])
+            first(
+                    tableEnv.sqlQuery(
+                        "SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POLYGON ((0 
0, 0 10, 10 10, 10 0, 0 0))'), 6)"))
+                .getField(0);
+    assertArrayEquals(new String[] {"122220", "122222", "122221", "122223"}, 
result);
+
+    // POINT EMPTY → empty list
+    result =
+        (String[])
+            first(tableEnv.sqlQuery("SELECT 
ST_BingTileCellIDs(ST_GeomFromWKT('POINT EMPTY'), 10)"))
+                .getField(0);
+    assertEquals(0, result.length);
+
+    // round-trip tile polygon back to the same tile
+    result =
+        (String[])
+            first(
+                    tableEnv.sqlQuery(
+                        "SELECT 
ST_BingTileCellIDs(ST_BingTilePolygon('1230301230'), 10)"))
+                .getField(0);
+    assertArrayEquals(new String[] {"1230301230"}, result);
+  }
+
+  @Test
+  public void testBingTileToGeom() {
+    Geometry[] result =
+        (Geometry[])
+            first(tableEnv.sqlQuery("SELECT ST_BingTileToGeom(ARRAY['0', '1', 
'2', '3'])"))
+                .getField(0);
+    assertEquals(4, result.length);
+    for (Geometry g : result) {
+      assertTrue(g instanceof Polygon);
+      assertEquals(5, g.getCoordinates().length);
+    }
+  }
 }
diff --git a/python/sedona/spark/sql/st_functions.py 
b/python/sedona/spark/sql/st_functions.py
index 34afd113b0..0a52d8c4c2 100644
--- a/python/sedona/spark/sql/st_functions.py
+++ b/python/sedona/spark/sql/st_functions.py
@@ -917,6 +917,143 @@ def ST_H3ToGeom(cells: Union[ColumnOrName, list]) -> 
Column:
     return _call_st_function("ST_H3ToGeom", cells)
 
 
+# =========================================================================
+# Bing Tile functions
+# =========================================================================
+
+
+@validate_argument_types
+def ST_BingTile(
+    tile_x: Union[ColumnOrName, int],
+    tile_y: Union[ColumnOrName, int],
+    zoom_level: Union[ColumnOrName, int],
+) -> Column:
+    """Create a Bing tile quadkey from XY coordinates and zoom level.
+    :param tile_x: Tile X coordinate
+    :type tile_x: Union[ColumnOrName, int]
+    :param tile_y: Tile Y coordinate
+    :type tile_y: Union[ColumnOrName, int]
+    :param zoom_level: Zoom level (1 to 23)
+    :type zoom_level: Union[ColumnOrName, int]
+    :return: Quadkey string
+    :rtype: Column
+    """
+    args = (tile_x, tile_y, zoom_level)
+    return _call_st_function("ST_BingTile", args)
+
+
+@validate_argument_types
+def ST_BingTileAt(
+    longitude: Union[ColumnOrName, float],
+    latitude: Union[ColumnOrName, float],
+    zoom_level: Union[ColumnOrName, int],
+) -> Column:
+    """Return the Bing tile quadkey containing the given point at the 
specified zoom level.
+    :param longitude: Longitude (-180 to 180)
+    :type longitude: Union[ColumnOrName, float]
+    :param latitude: Latitude (-85.05112878 to 85.05112878)
+    :type latitude: Union[ColumnOrName, float]
+    :param zoom_level: Zoom level (1 to 23)
+    :type zoom_level: Union[ColumnOrName, int]
+    :return: Quadkey string
+    :rtype: Column
+    """
+    args = (longitude, latitude, zoom_level)
+    return _call_st_function("ST_BingTileAt", args)
+
+
+@validate_argument_types
+def ST_BingTilesAround(
+    longitude: Union[ColumnOrName, float],
+    latitude: Union[ColumnOrName, float],
+    zoom_level: Union[ColumnOrName, int],
+) -> Column:
+    """Return the 3x3 neighborhood of Bing tiles around the tile containing 
the specified point.
+    :param longitude: Longitude
+    :type longitude: Union[ColumnOrName, float]
+    :param latitude: Latitude
+    :type latitude: Union[ColumnOrName, float]
+    :param zoom_level: Zoom level (1 to 23)
+    :type zoom_level: Union[ColumnOrName, int]
+    :return: Array of quadkey strings
+    :rtype: Column
+    """
+    args = (longitude, latitude, zoom_level)
+    return _call_st_function("ST_BingTilesAround", args)
+
+
+@validate_argument_types
+def ST_BingTileZoomLevel(quad_key: ColumnOrName) -> Column:
+    """Return the zoom level of a Bing tile quadkey.
+    :param quad_key: Quadkey string
+    :type quad_key: ColumnOrName
+    :return: Zoom level
+    :rtype: Column
+    """
+    return _call_st_function("ST_BingTileZoomLevel", quad_key)
+
+
+@validate_argument_types
+def ST_BingTileX(quad_key: ColumnOrName) -> Column:
+    """Return the X coordinate of a Bing tile from its quadkey.
+    :param quad_key: Quadkey string
+    :type quad_key: ColumnOrName
+    :return: Tile X coordinate
+    :rtype: Column
+    """
+    return _call_st_function("ST_BingTileX", quad_key)
+
+
+@validate_argument_types
+def ST_BingTileY(quad_key: ColumnOrName) -> Column:
+    """Return the Y coordinate of a Bing tile from its quadkey.
+    :param quad_key: Quadkey string
+    :type quad_key: ColumnOrName
+    :return: Tile Y coordinate
+    :rtype: Column
+    """
+    return _call_st_function("ST_BingTileY", quad_key)
+
+
+@validate_argument_types
+def ST_BingTilePolygon(quad_key: ColumnOrName) -> Column:
+    """Return the polygon representation of a Bing tile given its quadkey.
+    :param quad_key: Quadkey string
+    :type quad_key: ColumnOrName
+    :return: Tile polygon geometry
+    :rtype: Column
+    """
+    return _call_st_function("ST_BingTilePolygon", quad_key)
+
+
+@validate_argument_types
+def ST_BingTileCellIDs(
+    geometry: ColumnOrName,
+    zoom_level: Union[ColumnOrName, int],
+) -> Column:
+    """Return the minimum set of Bing tile quadkeys that fully cover a given 
geometry at the specified zoom level.
+    :param geometry: Geometry to cover
+    :type geometry: ColumnOrName
+    :param zoom_level: Zoom level (1 to 23)
+    :type zoom_level: Union[ColumnOrName, int]
+    :return: Array of quadkey strings
+    :rtype: Column
+    """
+    args = (geometry, zoom_level)
+    return _call_st_function("ST_BingTileCellIDs", args)
+
+
+@validate_argument_types
+def ST_BingTileToGeom(quad_keys: Union[ColumnOrName, list]) -> Column:
+    """Convert an array of Bing tile quadkeys to their polygon geometries.
+    :param quad_keys: Array of quadkey strings
+    :type quad_keys: Union[ColumnOrName, list]
+    :return: Array of polygon geometries
+    :rtype: Column
+    """
+    return _call_st_function("ST_BingTileToGeom", quad_keys)
+
+
 @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.
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 b1f24e15f1..2c226f70ba 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
@@ -1403,4 +1403,64 @@ public class TestFunctions extends TestBase {
         "SELECT 
sedona.GeometryType(sedona.ST_ApproximateMedialAxis(sedona.ST_GeomFromWKT('POLYGON
 ((0 0, 10 0, 10 5, 5 5, 5 10, 0 10, 0 0))'), 100))",
         "MULTILINESTRING");
   }
+
+  @Test
+  public void test_ST_BingTile() {
+    registerUDF("ST_BingTile", int.class, int.class, int.class);
+    verifySqlSingleRes("select sedona.ST_BingTile(3, 5, 3)", "213");
+  }
+
+  @Test
+  public void test_ST_BingTileAt() {
+    registerUDF("ST_BingTileAt", double.class, double.class, int.class);
+    verifySqlSingleRes("select sedona.ST_BingTileAt(60, 30.12, 15)", 
"123030123010121");
+  }
+
+  @Test
+  public void test_ST_BingTilesAround() {
+    registerUDF("ST_BingTilesAround", double.class, double.class, int.class);
+    verifySqlSingleRes("select ARRAY_SIZE(sedona.ST_BingTilesAround(60, 30.12, 
15))", 9);
+  }
+
+  @Test
+  public void test_ST_BingTileZoomLevel() {
+    registerUDF("ST_BingTileZoomLevel", String.class);
+    verifySqlSingleRes("select sedona.ST_BingTileZoomLevel('213')", 3);
+  }
+
+  @Test
+  public void test_ST_BingTileX() {
+    registerUDF("ST_BingTileX", String.class);
+    verifySqlSingleRes("select sedona.ST_BingTileX('213')", 3);
+  }
+
+  @Test
+  public void test_ST_BingTileY() {
+    registerUDF("ST_BingTileY", String.class);
+    verifySqlSingleRes("select sedona.ST_BingTileY('213')", 5);
+  }
+
+  @Test
+  public void test_ST_BingTilePolygon() {
+    registerUDF("ST_BingTilePolygon", String.class);
+    registerUDF("GeometryType", byte[].class);
+    verifySqlSingleRes("select 
sedona.GeometryType(sedona.ST_BingTilePolygon('213'))", "POLYGON");
+  }
+
+  @Test
+  public void test_ST_BingTileCellIDs() {
+    registerUDF("ST_BingTileCellIDs", byte[].class, int.class);
+    verifySqlSingleRes(
+        "select 
ARRAY_SIZE(sedona.ST_BingTileCellIDs(sedona.ST_GeomFromText('POINT(60 30.12)'), 
10))",
+        1);
+  }
+
+  @Test
+  public void test_ST_BingTileToGeom() {
+    registerUDF("ST_BingTileToGeom", String[].class);
+    registerUDF("GeometryType", byte[].class);
+    verifySqlSingleRes(
+        "select 
sedona.GeometryType(sedona.ST_BingTileToGeom(ARRAY_CONSTRUCT('0', '1')))",
+        "GEOMETRYCOLLECTION");
+  }
 }
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 ae26fedad2..67bc09b187 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
@@ -1344,4 +1344,64 @@ public class TestFunctionsV2 extends TestBase {
         "select 
sedona.GeometryType(sedona.ST_ApproximateMedialAxis(ST_GeometryFromWKT('POLYGON 
((0 0, 10 0, 10 5, 5 5, 5 10, 0 10, 0 0))'), 100))",
         "MULTILINESTRING");
   }
+
+  @Test
+  public void test_ST_BingTile() {
+    registerUDFV2("ST_BingTile", int.class, int.class, int.class);
+    verifySqlSingleRes("select sedona.ST_BingTile(3, 5, 3)", "213");
+  }
+
+  @Test
+  public void test_ST_BingTileAt() {
+    registerUDFV2("ST_BingTileAt", double.class, double.class, int.class);
+    verifySqlSingleRes("select sedona.ST_BingTileAt(60, 30.12, 15)", 
"123030123010121");
+  }
+
+  @Test
+  public void test_ST_BingTilesAround() {
+    registerUDFV2("ST_BingTilesAround", double.class, double.class, int.class);
+    verifySqlSingleRes("select ARRAY_SIZE(sedona.ST_BingTilesAround(60, 30.12, 
15))", 9);
+  }
+
+  @Test
+  public void test_ST_BingTileZoomLevel() {
+    registerUDFV2("ST_BingTileZoomLevel", String.class);
+    verifySqlSingleRes("select sedona.ST_BingTileZoomLevel('213')", 3);
+  }
+
+  @Test
+  public void test_ST_BingTileX() {
+    registerUDFV2("ST_BingTileX", String.class);
+    verifySqlSingleRes("select sedona.ST_BingTileX('213')", 3);
+  }
+
+  @Test
+  public void test_ST_BingTileY() {
+    registerUDFV2("ST_BingTileY", String.class);
+    verifySqlSingleRes("select sedona.ST_BingTileY('213')", 5);
+  }
+
+  @Test
+  public void test_ST_BingTilePolygon() {
+    registerUDFV2("ST_BingTilePolygon", String.class);
+    registerUDFV2("GeometryType", String.class);
+    verifySqlSingleRes("select 
sedona.GeometryType(sedona.ST_BingTilePolygon('213'))", "POLYGON");
+  }
+
+  @Test
+  public void test_ST_BingTileCellIDs() {
+    registerUDFV2("ST_BingTileCellIDs", String.class, int.class);
+    verifySqlSingleRes(
+        "select 
ARRAY_SIZE(sedona.ST_BingTileCellIDs(ST_GeometryFromWKT('POINT(60 30.12)'), 
10))",
+        1);
+  }
+
+  @Test
+  public void test_ST_BingTileToGeom() {
+    registerUDFV2("ST_BingTileToGeom", String[].class);
+    registerUDFV2("GeometryType", String.class);
+    verifySqlSingleRes(
+        "select 
sedona.GeometryType(sedona.ST_BingTileToGeom(ARRAY_CONSTRUCT('0', '1')))",
+        "GEOMETRYCOLLECTION");
+  }
 }
diff --git 
a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java 
b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
index b4cde280c4..a7d98082ea 100644
--- a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
+++ b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
@@ -1418,4 +1418,52 @@ public class UDFs {
     return GeometrySerde.serialize(
         Functions.rotate(GeometrySerde.deserialize(geom), angle, originX, 
originY));
   }
+
+  // Bing Tile functions
+
+  @UDFAnnotations.ParamMeta(argNames = {"tileX", "tileY", "zoomLevel"})
+  public static String ST_BingTile(int tileX, int tileY, int zoomLevel) {
+    return Functions.bingTile(tileX, tileY, zoomLevel);
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"longitude", "latitude", "zoomLevel"})
+  public static String ST_BingTileAt(double longitude, double latitude, int 
zoomLevel) {
+    return Functions.bingTileAt(longitude, latitude, zoomLevel);
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"longitude", "latitude", "zoomLevel"})
+  public static String[] ST_BingTilesAround(double longitude, double latitude, 
int zoomLevel) {
+    return Functions.bingTilesAround(longitude, latitude, zoomLevel);
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"quadKey"})
+  public static int ST_BingTileZoomLevel(String quadKey) {
+    return Functions.bingTileZoomLevel(quadKey);
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"quadKey"})
+  public static int ST_BingTileX(String quadKey) {
+    return Functions.bingTileX(quadKey);
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"quadKey"})
+  public static int ST_BingTileY(String quadKey) {
+    return Functions.bingTileY(quadKey);
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"quadKey"})
+  public static byte[] ST_BingTilePolygon(String quadKey) {
+    return GeometrySerde.serialize(Functions.bingTilePolygon(quadKey));
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"geometry", "zoomLevel"})
+  public static String[] ST_BingTileCellIDs(byte[] geometry, int zoomLevel) {
+    return Functions.bingTileCellIDs(GeometrySerde.deserialize(geometry), 
zoomLevel);
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"quadKeys"})
+  public static byte[] ST_BingTileToGeom(String[] quadKeys) {
+    Geometry[] geoms = Functions.bingTileToGeom(quadKeys);
+    return 
GeometrySerde.serialize(GeometrySerde.GEOMETRY_FACTORY.createGeometryCollection(geoms));
+  }
 }
diff --git 
a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java 
b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
index 7cff529ed3..8fcc980d8a 100644
--- a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
+++ b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
@@ -1751,4 +1751,65 @@ public class UDFsV2 {
     return GeometrySerde.serGeoJson(
         Functions.rotate(GeometrySerde.deserGeoJson(geom), angle, originX, 
originY));
   }
+
+  // Bing Tile functions
+
+  @UDFAnnotations.ParamMeta(argNames = {"tileX", "tileY", "zoomLevel"})
+  public static String ST_BingTile(int tileX, int tileY, int zoomLevel) {
+    return Functions.bingTile(tileX, tileY, zoomLevel);
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"longitude", "latitude", "zoomLevel"})
+  public static String ST_BingTileAt(double longitude, double latitude, int 
zoomLevel) {
+    return Functions.bingTileAt(longitude, latitude, zoomLevel);
+  }
+
+  @UDFAnnotations.ParamMeta(argNames = {"longitude", "latitude", "zoomLevel"})
+  public static String[] ST_BingTilesAround(double longitude, double latitude, 
int zoomLevel) {
+    return Functions.bingTilesAround(longitude, latitude, zoomLevel);
+  }
+
+  @UDFAnnotations.ParamMeta(
+      argNames = {"quadKey"},
+      argTypes = {"String"})
+  public static int ST_BingTileZoomLevel(String quadKey) {
+    return Functions.bingTileZoomLevel(quadKey);
+  }
+
+  @UDFAnnotations.ParamMeta(
+      argNames = {"quadKey"},
+      argTypes = {"String"})
+  public static int ST_BingTileX(String quadKey) {
+    return Functions.bingTileX(quadKey);
+  }
+
+  @UDFAnnotations.ParamMeta(
+      argNames = {"quadKey"},
+      argTypes = {"String"})
+  public static int ST_BingTileY(String quadKey) {
+    return Functions.bingTileY(quadKey);
+  }
+
+  @UDFAnnotations.ParamMeta(
+      argNames = {"quadKey"},
+      argTypes = {"String"},
+      returnTypes = "Geometry")
+  public static String ST_BingTilePolygon(String quadKey) {
+    return GeometrySerde.serGeoJson(Functions.bingTilePolygon(quadKey));
+  }
+
+  @UDFAnnotations.ParamMeta(
+      argNames = {"geometry", "zoomLevel"},
+      argTypes = {"Geometry", "int"})
+  public static String[] ST_BingTileCellIDs(String geometry, int zoomLevel) {
+    return Functions.bingTileCellIDs(GeometrySerde.deserGeoJson(geometry), 
zoomLevel);
+  }
+
+  @UDFAnnotations.ParamMeta(
+      argNames = {"quadKeys"},
+      returnTypes = "Geometry")
+  public static String ST_BingTileToGeom(String[] quadKeys) {
+    Geometry[] geoms = Functions.bingTileToGeom(quadKeys);
+    return 
GeometrySerde.serGeoJson(GeometrySerde.GEOMETRY_FACTORY.createGeometryCollection(geoms));
+  }
 }
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 a2d483398a..1c20dc5577 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
@@ -153,6 +153,15 @@ object Catalog extends AbstractCatalog with Logging {
     function[ST_H3CellIDs](),
     function[ST_H3ToGeom](),
     function[ST_H3KRing](),
+    function[ST_BingTile](),
+    function[ST_BingTileAt](),
+    function[ST_BingTilesAround](),
+    function[ST_BingTileZoomLevel](),
+    function[ST_BingTileX](),
+    function[ST_BingTileY](),
+    function[ST_BingTilePolygon](),
+    function[ST_BingTileCellIDs](),
+    function[ST_BingTileToGeom](),
     function[ST_InteriorRingN](),
     function[ST_InterpolatePoint](),
     function[ST_Dump](),
diff --git 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
index da470ef6ff..31beddc720 100644
--- 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
+++ 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
@@ -1547,6 +1547,83 @@ private[apache] case class ST_H3ToGeom(inputExpressions: 
Seq[Expression])
   }
 }
 
+// =========================================================================
+// Bing Tile expressions
+// =========================================================================
+
+private[apache] case class ST_BingTile(inputExpressions: Seq[Expression])
+    extends InferredExpression(Functions.bingTile _) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
+private[apache] case class ST_BingTileAt(inputExpressions: Seq[Expression])
+    extends InferredExpression(Functions.bingTileAt _) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
+private[apache] case class ST_BingTilesAround(inputExpressions: 
Seq[Expression])
+    extends InferredExpression(Functions.bingTilesAround _) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
+private[apache] case class ST_BingTileZoomLevel(inputExpressions: 
Seq[Expression])
+    extends InferredExpression(Functions.bingTileZoomLevel _) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
+private[apache] case class ST_BingTileX(inputExpressions: Seq[Expression])
+    extends InferredExpression(Functions.bingTileX _) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
+private[apache] case class ST_BingTileY(inputExpressions: Seq[Expression])
+    extends InferredExpression(Functions.bingTileY _) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
+private[apache] case class ST_BingTilePolygon(inputExpressions: 
Seq[Expression])
+    extends InferredExpression(Functions.bingTilePolygon _) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
+private[apache] case class ST_BingTileCellIDs(inputExpressions: 
Seq[Expression])
+    extends InferredExpression(Functions.bingTileCellIDs _) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
+private[apache] case class ST_BingTileToGeom(inputExpressions: Seq[Expression])
+    extends InferredExpression(Functions.bingTileToGeom _)
+    with FoldableExpression {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
 private[apache] case class ST_CollectionExtract(inputExpressions: 
Seq[Expression])
     extends 
InferredExpression(InferrableFunction.allowRightNull(Functions.collectionExtract))
 {
 
diff --git 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
index 818055b906..f79fc78be5 100644
--- 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
+++ 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
@@ -291,6 +291,46 @@ object st_functions {
 
   def ST_H3ToGeom(cellIds: Array[Long]): Column = 
wrapExpression[ST_H3ToGeom](cellIds)
 
+  // Bing Tile functions
+  def ST_BingTile(tileX: Column, tileY: Column, zoomLevel: Column): Column =
+    wrapExpression[ST_BingTile](tileX, tileY, zoomLevel)
+  def ST_BingTile(tileX: Int, tileY: Int, zoomLevel: Int): Column =
+    wrapExpression[ST_BingTile](tileX, tileY, zoomLevel)
+
+  def ST_BingTileAt(longitude: Column, latitude: Column, zoomLevel: Column): 
Column =
+    wrapExpression[ST_BingTileAt](longitude, latitude, zoomLevel)
+  def ST_BingTileAt(longitude: Double, latitude: Double, zoomLevel: Int): 
Column =
+    wrapExpression[ST_BingTileAt](longitude, latitude, zoomLevel)
+
+  def ST_BingTilesAround(longitude: Column, latitude: Column, zoomLevel: 
Column): Column =
+    wrapExpression[ST_BingTilesAround](longitude, latitude, zoomLevel)
+  def ST_BingTilesAround(longitude: Double, latitude: Double, zoomLevel: Int): 
Column =
+    wrapExpression[ST_BingTilesAround](longitude, latitude, zoomLevel)
+
+  def ST_BingTileZoomLevel(quadKey: Column): Column =
+    wrapExpression[ST_BingTileZoomLevel](quadKey)
+  def ST_BingTileZoomLevel(quadKey: String): Column =
+    wrapExpression[ST_BingTileZoomLevel](quadKey)
+
+  def ST_BingTileX(quadKey: Column): Column = 
wrapExpression[ST_BingTileX](quadKey)
+  def ST_BingTileX(quadKey: String): Column = 
wrapExpression[ST_BingTileX](quadKey)
+
+  def ST_BingTileY(quadKey: Column): Column = 
wrapExpression[ST_BingTileY](quadKey)
+  def ST_BingTileY(quadKey: String): Column = 
wrapExpression[ST_BingTileY](quadKey)
+
+  def ST_BingTilePolygon(quadKey: Column): Column =
+    wrapExpression[ST_BingTilePolygon](quadKey)
+  def ST_BingTilePolygon(quadKey: String): Column =
+    wrapExpression[ST_BingTilePolygon](quadKey)
+
+  def ST_BingTileCellIDs(geometry: Column, zoomLevel: Column): Column =
+    wrapExpression[ST_BingTileCellIDs](geometry, zoomLevel)
+  def ST_BingTileCellIDs(geometry: String, zoomLevel: Int): Column =
+    wrapExpression[ST_BingTileCellIDs](geometry, zoomLevel)
+
+  def ST_BingTileToGeom(quadKeys: Column): Column =
+    wrapExpression[ST_BingTileToGeom](quadKeys)
+
   def ST_InteriorRingN(polygon: Column, n: Column): Column =
     wrapExpression[ST_InteriorRingN](polygon, n)
   def ST_InteriorRingN(polygon: String, n: Int): Column =
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 8cdd05da15..d31da962dc 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
@@ -2659,5 +2659,112 @@ class dataFrameAPITestScala extends TestBaseScala {
       val medialAxis = result.asInstanceOf[Geometry]
       assert(medialAxis.getGeometryType == "MultiLineString")
     }
+
+    // Bing Tile functions
+    it("Passed ST_BingTile with literal overload") {
+      val df = sparkSession.sql("SELECT 1 AS dummy").select(ST_BingTile(3, 5, 
3))
+      assert(df.first().getString(0) == "213")
+    }
+
+    it("Passed ST_BingTile with Column overload") {
+      val df = sparkSession
+        .sql("SELECT 3 AS tileX, 5 AS tileY, 3 AS zoom")
+        .select(ST_BingTile(col("tileX"), col("tileY"), col("zoom")))
+      assert(df.first().getString(0) == "213")
+    }
+
+    it("Passed ST_BingTileAt with literal overload") {
+      val df = sparkSession.sql("SELECT 1 AS 
dummy").select(ST_BingTileAt(60.0, 30.12, 15))
+      assert(df.first().getString(0) == "123030123010121")
+    }
+
+    it("Passed ST_BingTileAt with Column overload") {
+      val df = sparkSession
+        .sql("SELECT 60.0 AS lon, 30.12 AS lat, 15 AS zoom")
+        .select(ST_BingTileAt(col("lon"), col("lat"), col("zoom")))
+      assert(df.first().getString(0) == "123030123010121")
+    }
+
+    it("Passed ST_BingTilesAround with literal overload") {
+      val df = sparkSession.sql("SELECT 1 AS 
dummy").select(ST_BingTilesAround(60.0, 30.12, 1))
+      val result = df.first().getAs[WrappedArray[String]](0)
+      assert(result == WrappedArray.make(Array("0", "2", "1", "3")))
+    }
+
+    it("Passed ST_BingTilesAround with Column overload") {
+      val df = sparkSession
+        .sql("SELECT 60.0 AS lon, 30.12 AS lat, 1 AS zoom")
+        .select(ST_BingTilesAround(col("lon"), col("lat"), col("zoom")))
+      val result = df.first().getAs[WrappedArray[String]](0)
+      assert(result == WrappedArray.make(Array("0", "2", "1", "3")))
+    }
+
+    it("Passed ST_BingTileZoomLevel with String column name") {
+      val df = sparkSession
+        .sql("SELECT '123030123010121' AS qk")
+        .select(ST_BingTileZoomLevel("qk"))
+      assert(df.first().getInt(0) == 15)
+    }
+
+    it("Passed ST_BingTileZoomLevel with Column overload") {
+      val df = sparkSession
+        .sql("SELECT '123030123010121' AS qk")
+        .select(ST_BingTileZoomLevel(col("qk")))
+      assert(df.first().getInt(0) == 15)
+    }
+
+    it("Passed ST_BingTileX with String column name") {
+      val df = sparkSession.sql("SELECT '213' AS 
qk").select(ST_BingTileX("qk"))
+      assert(df.first().getInt(0) == 3)
+    }
+
+    it("Passed ST_BingTileX with Column overload") {
+      val df = sparkSession.sql("SELECT '213' AS 
qk").select(ST_BingTileX(col("qk")))
+      assert(df.first().getInt(0) == 3)
+    }
+
+    it("Passed ST_BingTileY with String column name") {
+      val df = sparkSession.sql("SELECT '213' AS 
qk").select(ST_BingTileY("qk"))
+      assert(df.first().getInt(0) == 5)
+    }
+
+    it("Passed ST_BingTileY with Column overload") {
+      val df = sparkSession.sql("SELECT '213' AS 
qk").select(ST_BingTileY(col("qk")))
+      assert(df.first().getInt(0) == 5)
+    }
+
+    it("Passed ST_BingTilePolygon with String column name") {
+      val df = sparkSession
+        .sql("SELECT '123030123010121' AS qk")
+        .select(ST_BingTilePolygon("qk"))
+      val geom = df.first().get(0).asInstanceOf[Geometry]
+      assert(geom != null)
+      assert(geom.toText.startsWith("POLYGON"))
+    }
+
+    it("Passed ST_BingTilePolygon with Column overload") {
+      val df = sparkSession
+        .sql("SELECT '123030123010121' AS qk")
+        .select(ST_BingTilePolygon(col("qk")))
+      val geom = df.first().get(0).asInstanceOf[Geometry]
+      assert(geom != null)
+      assert(geom.toText.startsWith("POLYGON"))
+    }
+
+    it("Passed ST_BingTileCellIDs with Column overload") {
+      val df = sparkSession
+        .sql("SELECT ST_GeomFromWKT('POINT (60 30.12)') AS geom, 10 AS zoom")
+        .select(ST_BingTileCellIDs(col("geom"), col("zoom")))
+      val result = df.first().getAs[WrappedArray[String]](0)
+      assert(result == WrappedArray.make(Array("1230301230")))
+    }
+
+    it("Passed ST_BingTileToGeom with Column overload") {
+      val df = sparkSession
+        .sql("SELECT array('0', '1', '2', '3') AS qks")
+        .select(ST_BingTileToGeom(col("qks")))
+      val result = df.first().getAs[WrappedArray[Any]](0)
+      assert(result.length == 4)
+    }
   }
 }
diff --git 
a/spark/common/src/test/scala/org/apache/sedona/sql/functions/STBingTileFunctions.scala
 
b/spark/common/src/test/scala/org/apache/sedona/sql/functions/STBingTileFunctions.scala
new file mode 100644
index 0000000000..ba49646a7f
--- /dev/null
+++ 
b/spark/common/src/test/scala/org/apache/sedona/sql/functions/STBingTileFunctions.scala
@@ -0,0 +1,254 @@
+/*
+ * 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.
+ */
+package org.apache.sedona.sql.functions
+
+import org.apache.sedona.sql.{GeometrySample, TestBaseScala}
+import org.apache.spark.sql.functions.expr
+import org.scalatest.{GivenWhenThen, Matchers}
+
+import scala.collection.mutable
+
+// Bing Tile function tests
+class STBingTileFunctions
+    extends TestBaseScala
+    with Matchers
+    with GeometrySample
+    with GivenWhenThen {
+  import sparkSession.implicits._
+
+  describe("should pass ST_BingTile") {
+
+    it("should create tile (3,5,3) as quadkey '213'") {
+      val result = sparkSession.sql("SELECT ST_BingTile(3, 5, 
3)").collect()(0).getString(0)
+      result should equal("213")
+    }
+
+    it("should create tile (21845,13506,15) as quadkey '123030123010121'") {
+      val result =
+        sparkSession.sql("SELECT ST_BingTile(21845, 13506, 
15)").collect()(0).getString(0)
+      result should equal("123030123010121")
+    }
+  }
+
+  describe("should pass ST_BingTileAt") {
+
+    // ST_BingTileAt(lon, lat, zoom) — longitude first
+    it("should return quadkey '123030123010121' for (lon=60, lat=30.12, 
zoom=15)") {
+      val result =
+        sparkSession.sql("SELECT ST_BingTileAt(60.0, 30.12, 
15)").collect()(0).getString(0)
+      result should equal("123030123010121")
+    }
+
+    it("should return tile(0,1,1) for (lon=-0.002, lat=0, zoom=1)") {
+      val qk = sparkSession.sql("SELECT ST_BingTileAt(-0.002, 0.0, 
1)").collect()(0).getString(0)
+      val x = sparkSession.sql(s"SELECT 
ST_BingTileX('$qk')").collect()(0).getInt(0)
+      val y = sparkSession.sql(s"SELECT 
ST_BingTileY('$qk')").collect()(0).getInt(0)
+      x should equal(0)
+      y should equal(1)
+    }
+  }
+
+  describe("should pass ST_BingTilesAround") {
+
+    // ST_BingTilesAround(lon, lat, zoom) — longitude first
+    it("should return all 4 tiles at zoom 1 for (lon=60, lat=30.12)") {
+      val result = sparkSession
+        .sql("SELECT ST_BingTilesAround(60.0, 30.12, 1)")
+        .collect()(0)
+        .getAs[mutable.WrappedArray[String]](0)
+      result should equal(mutable.WrappedArray.make(Array("0", "2", "1", "3")))
+    }
+
+    it("should return 9 tiles at zoom 15 for (lon=60, lat=30.12)") {
+      val result = sparkSession
+        .sql("SELECT ST_BingTilesAround(60.0, 30.12, 15)")
+        .collect()(0)
+        .getAs[mutable.WrappedArray[String]](0)
+      result should equal(
+        mutable.WrappedArray.make(Array(
+          "123030123010102",
+          "123030123010120",
+          "123030123010122",
+          "123030123010103",
+          "123030123010121",
+          "123030123010123",
+          "123030123010112",
+          "123030123010130",
+          "123030123010132")))
+    }
+
+    it("should return 4 tiles at corner (lon=-180, lat=-85.05112878, zoom=3)") 
{
+      val result = sparkSession
+        .sql("SELECT ST_BingTilesAround(-180.0, -85.05112878, 3)")
+        .collect()(0)
+        .getAs[mutable.WrappedArray[String]](0)
+      result should equal(mutable.WrappedArray.make(Array("220", "222", "221", 
"223")))
+    }
+
+    it("should return 6 tiles at edge (lon=0, lat=-85.05112878, zoom=2)") {
+      val result = sparkSession
+        .sql("SELECT ST_BingTilesAround(0.0, -85.05112878, 2)")
+        .collect()(0)
+        .getAs[mutable.WrappedArray[String]](0)
+      result should equal(mutable.WrappedArray.make(Array("21", "23", "30", 
"32", "31", "33")))
+    }
+  }
+
+  describe("should pass ST_BingTileZoomLevel") {
+
+    it("should return zoom level 3 for quadkey '213'") {
+      val result =
+        sparkSession.sql("SELECT 
ST_BingTileZoomLevel('213')").collect()(0).getInt(0)
+      result should equal(3)
+    }
+
+    it("should return zoom level 15 for quadkey '123030123010121'") {
+      val result = sparkSession
+        .sql("SELECT ST_BingTileZoomLevel('123030123010121')")
+        .collect()(0)
+        .getInt(0)
+      result should equal(15)
+    }
+  }
+
+  describe("should pass ST_BingTileX and ST_BingTileY") {
+
+    it("should return X=3 and Y=5 for quadkey '213'") {
+      val x = sparkSession.sql("SELECT 
ST_BingTileX('213')").collect()(0).getInt(0)
+      val y = sparkSession.sql("SELECT 
ST_BingTileY('213')").collect()(0).getInt(0)
+      x should equal(3)
+      y should equal(5)
+    }
+
+    it("should return X=21845 and Y=13506 for quadkey '123030123010121'") {
+      val x =
+        sparkSession.sql("SELECT 
ST_BingTileX('123030123010121')").collect()(0).getInt(0)
+      val y =
+        sparkSession.sql("SELECT 
ST_BingTileY('123030123010121')").collect()(0).getInt(0)
+      x should equal(21845)
+      y should equal(13506)
+    }
+
+    it("should round-trip via ST_BingTile and ST_BingTileX/Y") {
+      val result = sparkSession
+        .sql("SELECT ST_BingTile(ST_BingTileX('123030123010121'), 
ST_BingTileY('123030123010121'), ST_BingTileZoomLevel('123030123010121'))")
+        .collect()(0)
+        .getString(0)
+      result should equal("123030123010121")
+    }
+  }
+
+  describe("should pass ST_BingTilePolygon") {
+
+    it("should return correct envelope for quadkey '123030123010121'") {
+      val result = sparkSession
+        .sql("SELECT ST_AsText(ST_BingTilePolygon('123030123010121'))")
+        .collect()(0)
+        .getString(0)
+      result should not be null
+      result should startWith("POLYGON")
+      result should include("59.996337890625")
+      result should include("60.00732421875")
+    }
+
+    it("should return correct envelope for tile(0,0,1)") {
+      val result = sparkSession
+        .sql("SELECT ST_AsText(ST_BingTilePolygon(ST_BingTile(0, 0, 1)))")
+        .collect()(0)
+        .getString(0)
+      result should include("-180")
+      result should include("85.051128")
+    }
+  }
+
+  describe("should pass ST_BingTileCellIDs") {
+
+    it("should return one tile for a point at zoom 10") {
+      val result = sparkSession
+        .sql("SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POINT (60 30.12)'), 
10)")
+        .collect()(0)
+        .getAs[mutable.WrappedArray[String]](0)
+      result should equal(mutable.WrappedArray.make(Array("1230301230")))
+    }
+
+    it("should return one tile for a point at zoom 15") {
+      val result = sparkSession
+        .sql("SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POINT (60 30.12)'), 
15)")
+        .collect()(0)
+        .getAs[mutable.WrappedArray[String]](0)
+      result should equal(mutable.WrappedArray.make(Array("123030123010121")))
+    }
+
+    it("should return 4 tiles for a 10x10 polygon at zoom 6") {
+      val result = sparkSession
+        .sql("SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POLYGON ((0 0, 0 10, 
10 10, 10 0, 0 0))'), 6)")
+        .collect()(0)
+        .getAs[mutable.WrappedArray[String]](0)
+      result should equal(
+        mutable.WrappedArray.make(Array("122220", "122222", "122221", 
"122223")))
+    }
+
+    // geometry_to_bing_tiles(POLYGON((10 10, -10 10, -20 -15, 10 10)), 3)
+    it("should return 3 tiles for a triangle at zoom 3") {
+      val result = sparkSession
+        .sql("SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POLYGON ((10 10, -10 
10, -20 -15, 10 10))'), 3)")
+        .collect()(0)
+        .getAs[mutable.WrappedArray[String]](0)
+      result should equal(mutable.WrappedArray.make(Array("033", "211", 
"122")))
+    }
+
+    it("should return empty array for empty geometry") {
+      val result = sparkSession
+        .sql("SELECT ST_BingTileCellIDs(ST_GeomFromWKT('POINT EMPTY'), 10)")
+        .collect()(0)
+        .getAs[mutable.WrappedArray[String]](0)
+      result.length should equal(0)
+    }
+
+    it("should round-trip tile polygon back to the same tile") {
+      val result = sparkSession
+        .sql("SELECT ST_BingTileCellIDs(ST_BingTilePolygon('1230301230'), 10)")
+        .collect()(0)
+        .getAs[mutable.WrappedArray[String]](0)
+      result should equal(mutable.WrappedArray.make(Array("1230301230")))
+    }
+
+    it("should return 4 child tiles when expanding tile to zoom+1") {
+      val result = sparkSession
+        .sql("SELECT ST_BingTileCellIDs(ST_BingTilePolygon('1230301230'), 11)")
+        .collect()(0)
+        .getAs[mutable.WrappedArray[String]](0)
+      result should equal(
+        mutable.WrappedArray.make(
+          Array("12303012300", "12303012302", "12303012301", "12303012303")))
+    }
+  }
+
+  describe("should pass ST_BingTileToGeom") {
+
+    it("should convert quadkeys to polygon geometries") {
+      val result = sparkSession
+        .sql("SELECT ST_BingTileToGeom(array('0', '1', '2', '3'))")
+        .collect()(0)
+        .getAs[mutable.WrappedArray[Any]](0)
+      result.length should equal(4)
+    }
+  }
+
+}

Reply via email to