فهرست منبع

Add Geohex aggregation on geo_shape field (#91956)

This commit adds support for geohex aggregation on geoshape fields using cartesian geometry.
Ignacio Vera 2 سال پیش
والد
کامیت
d448012a03
20فایلهای تغییر یافته به همراه2494 افزوده شده و 98 حذف شده
  1. 5 0
      docs/changelog/91956.yaml
  2. 6 31
      x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoGridAggAndQueryConsistencyIT.java
  3. 49 12
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java
  4. 351 0
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/H3CartesianGeometry.java
  5. 462 0
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/H3CartesianUtil.java
  6. 1 1
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/ShapeValues.java
  7. 15 5
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/GeoGridQueryBuilder.java
  8. 191 0
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/AbstractGeoHexGridTiler.java
  9. 146 0
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/BoundedGeoHexGridTiler.java
  10. 353 0
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoHexVisitor.java
  11. 43 0
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoShapeHexGridAggregator.java
  12. 69 0
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/UnboundedGeoHexGridTiler.java
  13. 10 0
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/SpatialPluginTests.java
  14. 241 0
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/common/H3CartesianUtilTests.java
  15. 43 27
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoGridTilerTestCase.java
  16. 214 0
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoHexTilerTests.java
  17. 198 0
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoHexVisitorTests.java
  18. 4 6
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoShapeGeoGridTestCase.java
  19. 88 0
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoShapeGeoHexGridAggregatorTests.java
  20. 5 16
      x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/GridAggregation.java

+ 5 - 0
docs/changelog/91956.yaml

@@ -0,0 +1,5 @@
+pr: 91956
+summary: Geohex aggregation on `geo_shape` field
+area: Geo
+type: feature
+issues: []

+ 6 - 31
x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoGridAggAndQueryConsistencyIT.java

@@ -33,6 +33,7 @@ import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentFactory;
 import org.elasticsearch.xcontent.XContentType;
 import org.elasticsearch.xpack.spatial.LocalStateSpatialPlugin;
+import org.elasticsearch.xpack.spatial.common.H3CartesianUtil;
 import org.elasticsearch.xpack.spatial.index.mapper.GeoShapeWithDocValuesFieldMapper;
 import org.elasticsearch.xpack.spatial.index.query.GeoGridQueryBuilder;
 import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.GeoHexGridAggregationBuilder;
@@ -87,6 +88,10 @@ public class GeoGridAggAndQueryConsistencyIT extends ESIntegTestCase {
         );
     }
 
+    public void testGeoShapeGeoHex() throws IOException {
+        doTestGeohexGrid(GeoShapeWithDocValuesFieldMapper.CONTENT_TYPE, () -> GeometryTestUtils.randomGeometryWithoutCircle(0, false));
+    }
+
     private void doTestGeohashGrid(String fieldType, Supplier<Geometry> randomGeometriesSupplier) throws IOException {
         doTestGrid(
             1,
@@ -124,43 +129,13 @@ public class GeoGridAggAndQueryConsistencyIT extends ESIntegTestCase {
             }
             return points;
         },
-            this::toGeoHexRectangle,
+            h3 -> H3CartesianUtil.toBoundingBox(H3.stringToH3(h3)),
             GeoHexGridAggregationBuilder::new,
             (s1, s2) -> new GeoGridQueryBuilder(s1).setGridId(GeoGridQueryBuilder.Grid.GEOHEX, s2),
             randomGeometriesSupplier
         );
     }
 
-    private Rectangle toGeoHexRectangle(String bucketKey) {
-        final long h3 = H3.stringToH3(bucketKey);
-        final CellBoundary boundary = H3.h3ToGeoBoundary(h3);
-        double minLat = Double.POSITIVE_INFINITY;
-        double minLon = Double.POSITIVE_INFINITY;
-        double maxLat = Double.NEGATIVE_INFINITY;
-        double maxLon = Double.NEGATIVE_INFINITY;
-        for (int i = 0; i < boundary.numPoints(); i++) {
-            final double boundaryLat = boundary.getLatLon(i).getLatDeg();
-            final double boundaryLon = boundary.getLatLon(i).getLonDeg();
-            minLon = Math.min(minLon, boundaryLon);
-            maxLon = Math.max(maxLon, boundaryLon);
-            minLat = Math.min(minLat, boundaryLat);
-            maxLat = Math.max(maxLat, boundaryLat);
-        }
-        final int resolution = H3.getResolution(h3);
-        if (H3.geoToH3(90, 0, resolution) == h3) {
-            // north pole
-            return new Rectangle(-180d, 180d, 90, minLat);
-        } else if (H3.geoToH3(-90, 0, resolution) == h3) {
-            // south pole
-            return new Rectangle(-180d, 180d, maxLat, -90);
-        } else if (maxLon - minLon > 180d) {
-            // crosses dateline
-            return new Rectangle(maxLon, minLon, maxLat, minLat);
-        } else {
-            return new Rectangle(minLon, maxLon, maxLat, minLat);
-        }
-    }
-
     private void doTestGrid(
         int minPrecision,
         int maxPrecision,

+ 49 - 12
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java

@@ -51,6 +51,7 @@ import org.elasticsearch.xpack.spatial.ingest.CircleProcessor;
 import org.elasticsearch.xpack.spatial.search.aggregations.GeoLineAggregationBuilder;
 import org.elasticsearch.xpack.spatial.search.aggregations.InternalGeoLine;
 import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.BoundedGeoHashGridTiler;
+import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.BoundedGeoHexGridTiler;
 import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.BoundedGeoTileGridTiler;
 import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.GeoGridTiler;
 import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.GeoHexCellIdSource;
@@ -58,9 +59,11 @@ import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.GeoHex
 import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.GeoHexGridAggregator;
 import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.GeoShapeCellIdSource;
 import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.GeoShapeHashGridAggregator;
+import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.GeoShapeHexGridAggregator;
 import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.GeoShapeTileGridAggregator;
 import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.InternalGeoHexGrid;
 import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.UnboundedGeoHashGridTiler;
+import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.UnboundedGeoHexGridTiler;
 import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.UnboundedGeoTileGridTiler;
 import org.elasticsearch.xpack.spatial.search.aggregations.metrics.CartesianBoundsAggregationBuilder;
 import org.elasticsearch.xpack.spatial.search.aggregations.metrics.CartesianBoundsAggregator;
@@ -310,12 +313,9 @@ public class SpatialPlugin extends Plugin implements ActionPlugin, MapperPlugin,
                 collectsFromSingleBucket,
                 metadata) -> {
                 if (GEO_GRID_AGG_FEATURE.check(getLicenseState())) {
-                    final GeoGridTiler tiler;
-                    if (geoBoundingBox.isUnbounded()) {
-                        tiler = new UnboundedGeoHashGridTiler(precision);
-                    } else {
-                        tiler = new BoundedGeoHashGridTiler(precision, geoBoundingBox);
-                    }
+                    final GeoGridTiler tiler = geoBoundingBox.isUnbounded()
+                        ? new UnboundedGeoHashGridTiler(precision)
+                        : new BoundedGeoHashGridTiler(precision, geoBoundingBox);
                     GeoShapeCellIdSource cellIdSource = new GeoShapeCellIdSource((GeoShapeValuesSource) valuesSource, tiler);
                     GeoShapeHashGridAggregator agg = new GeoShapeHashGridAggregator(
                         name,
@@ -353,12 +353,9 @@ public class SpatialPlugin extends Plugin implements ActionPlugin, MapperPlugin,
                 collectsFromSingleBucket,
                 metadata) -> {
                 if (GEO_GRID_AGG_FEATURE.check(getLicenseState())) {
-                    final GeoGridTiler tiler;
-                    if (geoBoundingBox.isUnbounded()) {
-                        tiler = new UnboundedGeoTileGridTiler(precision);
-                    } else {
-                        tiler = new BoundedGeoTileGridTiler(precision, geoBoundingBox);
-                    }
+                    final GeoGridTiler tiler = geoBoundingBox.isUnbounded()
+                        ? new UnboundedGeoTileGridTiler(precision)
+                        : new BoundedGeoTileGridTiler(precision, geoBoundingBox);
                     GeoShapeCellIdSource cellIdSource = new GeoShapeCellIdSource((GeoShapeValuesSource) valuesSource, tiler);
                     GeoShapeTileGridAggregator agg = new GeoShapeTileGridAggregator(
                         name,
@@ -379,6 +376,46 @@ public class SpatialPlugin extends Plugin implements ActionPlugin, MapperPlugin,
             },
             true
         );
+
+        builder.register(
+            GeoHexGridAggregationBuilder.REGISTRY_KEY,
+            GeoShapeValuesSourceType.instance(),
+            (
+                name,
+                factories,
+                valuesSource,
+                precision,
+                geoBoundingBox,
+                requiredSize,
+                shardSize,
+                context,
+                parent,
+                collectsFromSingleBucket,
+                metadata) -> {
+                if (GEO_GRID_AGG_FEATURE.check(getLicenseState())) {
+                    final GeoGridTiler tiler = geoBoundingBox.isUnbounded()
+                        ? new UnboundedGeoHexGridTiler(precision)
+                        : new BoundedGeoHexGridTiler(precision, geoBoundingBox);
+                    GeoShapeCellIdSource cellIdSource = new GeoShapeCellIdSource((GeoShapeValuesSource) valuesSource, tiler);
+                    GeoShapeHexGridAggregator agg = new GeoShapeHexGridAggregator(
+                        name,
+                        factories,
+                        cellIdSource,
+                        requiredSize,
+                        shardSize,
+                        context,
+                        parent,
+                        collectsFromSingleBucket,
+                        metadata
+                    );
+                    // this would ideally be something set in an immutable way on the ValuesSource
+                    cellIdSource.setCircuitBreakerConsumer(agg::addRequestBytes);
+                    return agg;
+                }
+                throw LicenseUtils.newComplianceException("geohex_grid aggregation on geo_shape fields");
+            },
+            true
+        );
     }
 
     private static void registerValueCountAggregator(ValuesSourceRegistry.Builder builder) {

+ 351 - 0
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/H3CartesianGeometry.java

@@ -0,0 +1,351 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.spatial.common;
+
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.LatLonGeometry;
+import org.apache.lucene.geo.Rectangle;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.util.ArrayUtil;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoRelation;
+
+/**
+ * Lucene geometry representing an H3 bin on the cartesian space.
+ */
+class H3CartesianGeometry extends LatLonGeometry {
+
+    private final long h3;
+
+    H3CartesianGeometry(long h3) {
+        this.h3 = h3;
+    }
+
+    @Override
+    protected Component2D toComponent2D() {
+        return new H3CartesianComponent(h3);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        H3CartesianGeometry that = (H3CartesianGeometry) o;
+        return h3 == that.h3;
+    }
+
+    @Override
+    public int hashCode() {
+        return Long.hashCode(h3);
+    }
+
+    private static class H3CartesianComponent implements Component2D {
+
+        private final double[] xs, ys;
+        private final double minX, maxX, minY, maxY;
+        private final boolean crossesDateline;
+
+        H3CartesianComponent(long h3) {
+            final double[] xs = new double[H3CartesianUtil.MAX_ARRAY_SIZE];
+            final double[] ys = new double[H3CartesianUtil.MAX_ARRAY_SIZE];
+            final int numPoints = H3CartesianUtil.computePoints(h3, xs, ys);
+            this.xs = ArrayUtil.copyOfSubArray(xs, 0, numPoints);
+            this.ys = ArrayUtil.copyOfSubArray(ys, 0, numPoints);
+            double minX = Double.POSITIVE_INFINITY;
+            double maxX = Double.NEGATIVE_INFINITY;
+            double minY = Double.POSITIVE_INFINITY;
+            double maxY = Double.NEGATIVE_INFINITY;
+            for (int i = 0; i < numPoints; i++) {
+                minX = Math.min(minX, xs[i]);
+                maxX = Math.max(maxX, xs[i]);
+                minY = Math.min(minY, ys[i]);
+                maxY = Math.max(maxY, ys[i]);
+            }
+            this.minX = minX;
+            this.maxX = maxX;
+            this.minY = minY;
+            this.maxY = maxY;
+            this.crossesDateline = maxX - minX > 180d && H3CartesianUtil.isPolar(h3) == false;
+        }
+
+        @Override
+        public double getMinX() {
+            return crossesDateline ? -180d : minX;
+        }
+
+        @Override
+        public double getMaxX() {
+            return crossesDateline ? 180d : maxX;
+        }
+
+        @Override
+        public double getMinY() {
+            return minY;
+        }
+
+        @Override
+        public double getMaxY() {
+            return maxY;
+        }
+
+        @Override
+        public boolean contains(double x, double y) {
+            // fail fast if we're outside the bounding box
+            if (Rectangle.containsPoint(y, x, getMinY(), getMaxY(), getMinX(), getMaxX()) == false) {
+                return false;
+            }
+            return H3CartesianUtil.relatePoint(xs, ys, xs.length, crossesDateline, x, y) != GeoRelation.QUERY_DISJOINT;
+        }
+
+        @Override
+        public PointValues.Relation relate(double minX, double maxX, double minY, double maxY) {
+            if (Component2D.disjoint(getMinX(), getMaxX(), getMinY(), getMaxY(), minX, maxX, minY, maxY)) {
+                return PointValues.Relation.CELL_OUTSIDE_QUERY;
+            }
+            if (Component2D.within(getMinX(), getMaxX(), getMinY(), getMaxY(), minX, maxX, minY, maxY)) {
+                return PointValues.Relation.CELL_CROSSES_QUERY;
+            }
+            int numCorners = numberOfCorners(minX, maxX, minY, maxY);
+            if (numCorners == 4) {
+                // need to check in case we are crossing the dateline
+                if (crossesDateline && H3CartesianUtil.crossesBox(xs, ys, xs.length, crossesDateline, minX, maxX, minY, maxY, true)) {
+                    return PointValues.Relation.CELL_CROSSES_QUERY;
+                }
+                return PointValues.Relation.CELL_INSIDE_QUERY;
+            } else if (numCorners == 0) {
+                if (Component2D.containsPoint(xs[0], ys[0], minX, maxX, minY, maxY)) {
+                    return PointValues.Relation.CELL_CROSSES_QUERY;
+                }
+                if (H3CartesianUtil.crossesBox(xs, ys, xs.length, crossesDateline, minX, maxX, minY, maxY, true)) {
+                    return PointValues.Relation.CELL_CROSSES_QUERY;
+                }
+                return PointValues.Relation.CELL_OUTSIDE_QUERY;
+            }
+            return PointValues.Relation.CELL_CROSSES_QUERY;
+        }
+
+        @Override
+        public boolean intersectsLine(double minX, double maxX, double minY, double maxY, double aX, double aY, double bX, double bY) {
+            return contains(aX, aY) || contains(bX, bY) || crossesLine(minX, maxX, minY, maxY, aX, aY, bX, bY, true);
+        }
+
+        @Override
+        public boolean intersectsTriangle(
+            double minX,
+            double maxX,
+            double minY,
+            double maxY,
+            double aX,
+            double aY,
+            double bX,
+            double bY,
+            double cX,
+            double cY
+        ) {
+            if (Component2D.disjoint(getMinX(), getMaxX(), getMinY(), getMaxY(), minX, maxX, minY, maxY)) {
+                return false;
+            }
+            return contains(aX, aY)
+                || contains(bX, bY)
+                || contains(cX, cY)
+                || Component2D.pointInTriangle(minX, maxX, minY, maxY, xs[0], ys[0], aX, aY, bX, bY, cX, cY)
+                || H3CartesianUtil.crossesTriangle(
+                    xs,
+                    ys,
+                    xs.length,
+                    crossesDateline,
+                    minX,
+                    maxX,
+                    minY,
+                    maxY,
+                    aX,
+                    aY,
+                    bX,
+                    bY,
+                    cX,
+                    cY,
+                    true
+                );
+        }
+
+        @Override
+        public boolean containsLine(double minX, double maxX, double minY, double maxY, double aX, double aY, double bX, double bY) {
+            return contains(aX, aY) && contains(bX, bY) && crossesLine(minX, maxX, minY, maxY, aX, aY, bX, bY, false) == false;
+        }
+
+        @Override
+        public boolean containsTriangle(
+            double minX,
+            double maxX,
+            double minY,
+            double maxY,
+            double aX,
+            double aY,
+            double bX,
+            double bY,
+            double cX,
+            double cY
+        ) {
+            return (contains(aX, aY)
+                && contains(bX, bY)
+                && contains(cX, cY)
+                && H3CartesianUtil.crossesTriangle(
+                    xs,
+                    ys,
+                    xs.length,
+                    crossesDateline,
+                    minX,
+                    maxX,
+                    minY,
+                    maxY,
+                    aX,
+                    aY,
+                    bX,
+                    bY,
+                    cX,
+                    cY,
+                    false
+                ) == false);
+
+        }
+
+        @Override
+        public WithinRelation withinPoint(double x, double y) {
+            return contains(x, y) ? WithinRelation.NOTWITHIN : WithinRelation.DISJOINT;
+        }
+
+        @Override
+        public WithinRelation withinLine(
+            double minX,
+            double maxX,
+            double minY,
+            double maxY,
+            double aX,
+            double aY,
+            boolean ab,
+            double bX,
+            double bY
+        ) {
+            if (Component2D.disjoint(getMinX(), getMaxX(), getMinY(), getMaxY(), minX, maxX, minY, maxY)) {
+                return WithinRelation.DISJOINT;
+            }
+            if (contains(aX, aY) || contains(bX, bY)) {
+                return WithinRelation.NOTWITHIN;
+            }
+            if (ab && crossesLine(minX, maxX, minY, maxY, aX, aY, bX, bY, true)) {
+                return WithinRelation.NOTWITHIN;
+            }
+            return WithinRelation.DISJOINT;
+        }
+
+        @Override
+        public WithinRelation withinTriangle(
+            double minX,
+            double maxX,
+            double minY,
+            double maxY,
+            double aX,
+            double aY,
+            boolean ab,
+            double bX,
+            double bY,
+            boolean bc,
+            double cX,
+            double cY,
+            boolean ca
+        ) {
+            if (Component2D.disjoint(getMinX(), getMaxX(), getMinY(), getMaxY(), minX, maxX, minY, maxY)) {
+                return WithinRelation.DISJOINT;
+            }
+
+            // if any of the points is inside the polygon, the polygon cannot be within this indexed
+            // shape because points belong to the original indexed shape.
+            if (contains(aX, aY) || contains(bX, bY) || contains(cX, cY)) {
+                return WithinRelation.NOTWITHIN;
+            }
+
+            WithinRelation relation = WithinRelation.DISJOINT;
+            // if any of the edges intersects and the edge belongs to the shape then it cannot be within.
+            // if it only intersects edges that do not belong to the shape, then it is a candidate
+            // we skip edges at the dateline to support shapes crossing it
+            if (crossesLine(minX, maxX, minY, maxY, aX, aY, bX, bY, true)) {
+                if (ab) {
+                    return WithinRelation.NOTWITHIN;
+                } else {
+                    relation = WithinRelation.CANDIDATE;
+                }
+            }
+
+            if (crossesLine(minX, maxX, minY, maxY, bX, bY, cX, cY, true)) {
+                if (bc) {
+                    return WithinRelation.NOTWITHIN;
+                } else {
+                    relation = WithinRelation.CANDIDATE;
+                }
+            }
+            if (crossesLine(minX, maxX, minY, maxY, cX, cY, aX, aY, true)) {
+                if (ca) {
+                    return WithinRelation.NOTWITHIN;
+                } else {
+                    relation = WithinRelation.CANDIDATE;
+                }
+            }
+
+            // if any of the edges crosses and edge that does not belong to the shape
+            // then it is a candidate for within
+            if (relation == WithinRelation.CANDIDATE) {
+                return WithinRelation.CANDIDATE;
+            }
+
+            // Check if shape is within the triangle
+            if (Component2D.pointInTriangle(minX, maxX, minY, maxY, xs[0], ys[0], aX, aY, bX, bY, cX, cY)) {
+                return WithinRelation.CANDIDATE;
+            }
+            return relation;
+        }
+
+        private boolean crossesLine(
+            double minX,
+            double maxX,
+            double minY,
+            double maxY,
+            double aX,
+            double aY,
+            double bX,
+            double bY,
+            boolean includeBoundary
+        ) {
+            if (Component2D.disjoint(getMinX(), getMaxX(), getMinY(), getMaxY(), minX, maxX, minY, maxY)) {
+                return false;
+            }
+            return H3CartesianUtil.crossesLine(xs, ys, xs.length, crossesDateline, minX, maxX, minY, maxY, aX, aY, bX, bY, includeBoundary);
+        }
+
+        private int numberOfCorners(double minX, double maxX, double minY, double maxY) {
+            int containsCount = 0;
+            if (contains(minX, minY)) {
+                containsCount++;
+            }
+            if (contains(maxX, minY)) {
+                containsCount++;
+            }
+            if (containsCount == 1) {
+                return containsCount;
+            }
+            if (contains(maxX, maxY)) {
+                containsCount++;
+            }
+            if (containsCount == 2) {
+                return containsCount;
+            }
+            if (contains(minX, maxY)) {
+                containsCount++;
+            }
+            return containsCount;
+        }
+    }
+}

+ 462 - 0
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/H3CartesianUtil.java

@@ -0,0 +1,462 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.spatial.common;
+
+import org.apache.lucene.geo.GeoEncodingUtils;
+import org.apache.lucene.geo.GeoUtils;
+import org.apache.lucene.geo.LatLonGeometry;
+import org.apache.lucene.geo.Rectangle;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.IntroSorter;
+import org.elasticsearch.h3.CellBoundary;
+import org.elasticsearch.h3.H3;
+import org.elasticsearch.h3.LatLng;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoRelation;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.DoubleUnaryOperator;
+
+import static org.apache.lucene.geo.GeoUtils.lineCrossesLine;
+import static org.apache.lucene.geo.GeoUtils.lineCrossesLineWithBoundary;
+
+/**
+ * Utility class that generates H3 bins coordinates projected on the cartesian plane (equirectangular projection).
+ * Provides spatial methods to compute spatial intersections on those coordinates.
+ */
+public final class H3CartesianUtil {
+    public static final int MAX_ARRAY_SIZE = 15;
+    private static final DoubleUnaryOperator NORMALIZE_LONG_POS = lon -> lon < 0 ? lon + 360d : lon;
+    private static final DoubleUnaryOperator NORMALIZE_LONG_NEG = lon -> lon > 0 ? lon - 360d : lon;
+    private static final long[] NORTH = new long[16];
+    private static final long[] SOUTH = new long[16];
+    static {
+        for (int res = 0; res <= H3.MAX_H3_RES; res++) {
+            NORTH[res] = H3.geoToH3(90, 0, res);
+            SOUTH[res] = H3.geoToH3(-90, 0, res);
+        }
+    }
+    // we cache the first two levels and polar polygons
+    private static final Map<Long, double[][]> CACHED_H3 = new HashMap<>();
+    static {
+        for (long res0Cell : H3.getLongRes0Cells()) {
+            CACHED_H3.put(res0Cell, getCoordinates(res0Cell));
+            for (long h3 : H3.h3ToChildren(res0Cell)) {
+                CACHED_H3.put(h3, getCoordinates(h3));
+            }
+        }
+        for (int res = 2; res <= H3.MAX_H3_RES; res++) {
+            CACHED_H3.put(NORTH[res], getCoordinates(NORTH[res]));
+            CACHED_H3.put(SOUTH[res], getCoordinates(SOUTH[res]));
+        }
+    }
+
+    private static final double[] NORTH_BOUND = new double[16];
+    private static final double[] SOUTH_BOUND = new double[16];
+    static {
+        for (int res = 0; res <= H3.MAX_H3_RES; res++) {
+            NORTH_BOUND[res] = toBoundingBox(NORTH[res]).getMinY();
+            SOUTH_BOUND[res] = toBoundingBox(SOUTH[res]).getMaxY();
+        }
+    }
+
+    /** For the given resolution, it returns the maximum latitude of the h3 bin containing the south pole */
+    public static boolean isPolar(long h3) {
+        final int resolution = H3.getResolution(h3);
+        return SOUTH[resolution] == h3 || NORTH[resolution] == h3;
+    }
+
+    /** For the given resolution, it returns the maximum latitude of the h3 bin containing the south pole */
+    public static double getSouthPolarBound(int resolution) {
+        return SOUTH_BOUND[resolution];
+    }
+
+    /** For the given resolution, it returns the minimum latitude of the h3 bin containing the north pole */
+    public static double getNorthPolarBound(int resolution) {
+        return NORTH_BOUND[resolution];
+    }
+
+    private static double[][] getCoordinates(final long h3) {
+        final double[] xs = new double[MAX_ARRAY_SIZE];
+        final double[] ys = new double[MAX_ARRAY_SIZE];
+        final int numPoints = computePoints(h3, xs, ys);
+        return new double[][] { ArrayUtil.copyOfSubArray(xs, 0, numPoints), ArrayUtil.copyOfSubArray(ys, 0, numPoints), };
+    }
+
+    /** It stores the points for the given h3 in the provided arrays.The arrays
+     * should be at least have the length of {@link #MAX_ARRAY_SIZE}. It returns the number of point added. */
+    public static int computePoints(final long h3, final double[] xs, final double[] ys) {
+        final double[][] cached = CACHED_H3.get(h3);
+        if (cached != null) {
+            System.arraycopy(cached[0], 0, xs, 0, cached[0].length);
+            System.arraycopy(cached[1], 0, ys, 0, cached[0].length);
+            return cached[0].length;
+        }
+        final int resolution = H3.getResolution(h3);
+        final double pole = NORTH[resolution] == h3 ? GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(90d))
+            : SOUTH[resolution] == h3 ? GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(-90d))
+            : Double.NaN;
+        final CellBoundary cellBoundary = H3.h3ToGeoBoundary(h3);
+        final int numPoints;
+        if (Double.isNaN(pole)) {
+            numPoints = cellBoundary.numPoints() + 1;
+        } else {
+            numPoints = cellBoundary.numPoints() + 5;
+        }
+        for (int i = 0; i < cellBoundary.numPoints(); i++) {
+            final LatLng latLng = cellBoundary.getLatLon(i);
+            xs[i] = GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(latLng.getLonDeg()));
+            ys[i] = GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(latLng.getLatDeg()));
+        }
+        if (Double.isNaN(pole)) {
+            xs[cellBoundary.numPoints()] = xs[0];
+            ys[cellBoundary.numPoints()] = ys[0];
+        } else {
+            closePolarComponent(xs, ys, cellBoundary.numPoints(), pole);
+        }
+        return numPoints;
+    }
+
+    private static void closePolarComponent(double[] xs, double[] ys, int numBoundaryPoints, double pole) {
+        sort(xs, ys, numBoundaryPoints);
+        assert xs[0] > 0 != xs[numBoundaryPoints - 1] > 0 : "expected first and last element with different sign";
+        final double y = datelineIntersectionLatitude(xs[0], ys[0], xs[numBoundaryPoints - 1], ys[numBoundaryPoints - 1]);
+        xs[numBoundaryPoints] = GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.MAX_LON_ENCODED);
+        ys[numBoundaryPoints] = y;
+        xs[numBoundaryPoints + 1] = GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.MAX_LON_ENCODED);
+        ys[numBoundaryPoints + 1] = pole;
+        xs[numBoundaryPoints + 2] = GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.MIN_LON_ENCODED);
+        ys[numBoundaryPoints + 2] = pole;
+        xs[numBoundaryPoints + 3] = GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.MIN_LON_ENCODED);
+        ys[numBoundaryPoints + 3] = y;
+        xs[numBoundaryPoints + 4] = xs[0];
+        ys[numBoundaryPoints + 4] = ys[0];
+    }
+
+    private static void sort(double[] xs, double[] ys, int length) {
+        new IntroSorter() {
+            int pivotPos = -1;
+
+            @Override
+            protected void swap(int i, int j) {
+                double tmp = xs[i];
+                xs[i] = xs[j];
+                xs[j] = tmp;
+                tmp = ys[i];
+                ys[i] = ys[j];
+                ys[j] = tmp;
+            }
+
+            @Override
+            protected void setPivot(int i) {
+                pivotPos = i;
+            }
+
+            @Override
+            protected int comparePivot(int j) {
+                // all xs are different
+                return Double.compare(xs[pivotPos], xs[j]);
+            }
+        }.sort(0, length);
+    }
+
+    private static double datelineIntersectionLatitude(double x1, double y1, double x2, double y2) {
+        final double t = (180d - NORMALIZE_LONG_POS.applyAsDouble(x1)) / (NORMALIZE_LONG_POS.applyAsDouble(x2) - NORMALIZE_LONG_POS
+            .applyAsDouble(x1));
+        assert t > 0 && t <= 1;
+        return y1 + t * (y2 - y1);
+    }
+
+    /** Return the {@link LatLonGeometry} representing the provided H3 bin */
+    public static LatLonGeometry getLatLonGeometry(long h3) {
+        return new H3CartesianGeometry(h3);
+    }
+
+    /** Return the bounding box of the provided H3 bin */
+    public static org.elasticsearch.geometry.Rectangle toBoundingBox(long h3) {
+        final CellBoundary boundary = H3.h3ToGeoBoundary(h3);
+        double minLat = Double.POSITIVE_INFINITY;
+        double minLon = Double.POSITIVE_INFINITY;
+        double maxLat = Double.NEGATIVE_INFINITY;
+        double maxLon = Double.NEGATIVE_INFINITY;
+        for (int i = 0; i < boundary.numPoints(); i++) {
+            final LatLng latLng = boundary.getLatLon(i);
+            minLat = Math.min(minLat, latLng.getLatDeg());
+            minLon = Math.min(minLon, latLng.getLonDeg());
+            maxLat = Math.max(maxLat, latLng.getLatDeg());
+            maxLon = Math.max(maxLon, latLng.getLonDeg());
+        }
+        final int res = H3.getResolution(h3);
+        if (h3 == NORTH[res]) {
+            return new org.elasticsearch.geometry.Rectangle(-180d, 180d, 90d, minLat);
+        } else if (h3 == SOUTH[res]) {
+            return new org.elasticsearch.geometry.Rectangle(-180d, 180d, maxLat, -90d);
+        } else if (maxLon - minLon > 180d) {
+            return new org.elasticsearch.geometry.Rectangle(maxLon, minLon, maxLat, minLat);
+        } else {
+            return new org.elasticsearch.geometry.Rectangle(minLon, maxLon, maxLat, minLat);
+        }
+    }
+
+    /** Return the spatial relationship between an H3 and a point.*/
+    public static GeoRelation relatePoint(double[] xs, double[] ys, int numPoints, boolean crossesDateline, double x, double y) {
+        final DoubleUnaryOperator normalizeLong = crossesDateline ? NORMALIZE_LONG_POS : DoubleUnaryOperator.identity();
+        return relatePoint(xs, ys, numPoints, x, y, normalizeLong);
+    }
+
+    private static GeoRelation relatePoint(double[] xs, double[] ys, int numPoints, double x, double y, DoubleUnaryOperator normalize_lon) {
+        boolean res = false;
+        x = normalize_lon.applyAsDouble(x);
+        for (int i = 0; i < numPoints - 1; i++) {
+            final double x1 = normalize_lon.applyAsDouble(xs[i]);
+            final double x2 = normalize_lon.applyAsDouble(xs[i + 1]);
+            final double y1 = ys[i];
+            final double y2 = ys[i + 1];
+            if (y == y1 && y == y2 || (y <= y1 && y >= y2) != (y >= y1 && y <= y2)) {
+                if ((x == x1 && x == x2) || ((x <= x1 && x >= x2) != (x >= x1 && x <= x2) && GeoUtils.orient(x1, y1, x2, y2, x, y) == 0)) {
+                    return GeoRelation.QUERY_CROSSES;
+                } else if (y1 > y != y2 > y) {
+                    res ^= x < (x2 - x1) * (y - y1) / (y2 - y1) + x1;
+                }
+            }
+        }
+        return res ? GeoRelation.QUERY_CONTAINS : GeoRelation.QUERY_DISJOINT;
+    }
+
+    /** Checks if a line crosses a h3 bin.*/
+    public static boolean crossesLine(
+        double[] xs,
+        double[] ys,
+        int numPoints,
+        boolean crossesDateline,
+        double minX,
+        double maxX,
+        double minY,
+        double maxY,
+        double aX,
+        double aY,
+        double bX,
+        double bY,
+        boolean includeBoundary
+    ) {
+        if (crossesDateline) {
+            return crossesLine(xs, ys, numPoints, minX, maxX, minY, maxY, aX, aY, bX, bY, includeBoundary, NORMALIZE_LONG_POS)
+                || crossesLine(xs, ys, numPoints, minX, maxX, minY, maxY, aX, aY, bX, bY, includeBoundary, NORMALIZE_LONG_NEG);
+        } else {
+            return crossesLine(xs, ys, numPoints, minX, maxX, minY, maxY, aX, aY, bX, bY, includeBoundary, DoubleUnaryOperator.identity());
+        }
+    }
+
+    private static boolean crossesLine(
+        double[] xs,
+        double[] ys,
+        int numPoints,
+        double minX,
+        double maxX,
+        double minY,
+        double maxY,
+        double aX,
+        double aY,
+        double bX,
+        double bY,
+        boolean includeBoundary,
+        DoubleUnaryOperator normalizeLong
+    ) {
+
+        for (int i = 0; i < numPoints - 1; i++) {
+            double cy = ys[i];
+            double dy = ys[i + 1];
+            double cx = normalizeLong.applyAsDouble(xs[i]);
+            double dx = normalizeLong.applyAsDouble(xs[i + 1]);
+            // compute bounding box of line
+            double lMinX = StrictMath.min(cx, dx);
+            double lMaxX = StrictMath.max(cx, dx);
+            double lMinY = StrictMath.min(cy, dy);
+            double lMaxY = StrictMath.max(cy, dy);
+
+            // 2. check bounding boxes are disjoint
+            if (lMaxX < minX || lMinX > maxX || lMinY > maxY || lMaxY < minY) {
+                continue;
+            }
+            if (includeBoundary) {
+                if (GeoUtils.lineCrossesLineWithBoundary(cx, cy, dx, dy, aX, aY, bX, bY)) {
+                    return true;
+                }
+            } else {
+                if (GeoUtils.lineCrossesLine(cx, cy, dx, dy, aX, aY, bX, bY)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /** Checks if a triangle crosses a h3 bin.*/
+    public static boolean crossesTriangle(
+        double[] xs,
+        double[] ys,
+        int numPoints,
+        boolean crossesDateline,
+        double minX,
+        double maxX,
+        double minY,
+        double maxY,
+        double ax,
+        double ay,
+        double bx,
+        double by,
+        double cx,
+        double cy,
+        boolean includeBoundary
+    ) {
+        if (crossesDateline) {
+            return crossesTriangle(xs, ys, numPoints, minX, maxX, minY, maxY, ax, ay, bx, by, cx, cy, includeBoundary, NORMALIZE_LONG_POS)
+                || crossesTriangle(xs, ys, numPoints, minX, maxX, minY, maxY, ax, ay, bx, by, cx, cy, includeBoundary, NORMALIZE_LONG_NEG);
+        } else {
+            return crossesTriangle(
+                xs,
+                ys,
+                numPoints,
+                minX,
+                maxX,
+                minY,
+                maxY,
+                ax,
+                ay,
+                bx,
+                by,
+                cx,
+                cy,
+                includeBoundary,
+                DoubleUnaryOperator.identity()
+            );
+        }
+    }
+
+    private static boolean crossesTriangle(
+        double[] xs,
+        double[] ys,
+        int numPoints,
+        double minX,
+        double maxX,
+        double minY,
+        double maxY,
+        double ax,
+        double ay,
+        double bx,
+        double by,
+        double cx,
+        double cy,
+        boolean includeBoundary,
+        DoubleUnaryOperator normalizeLong
+    ) {
+        for (int i = 0; i < numPoints - 1; i++) {
+            double dy = ys[i];
+            double ey = ys[i + 1];
+            double dx = normalizeLong.applyAsDouble(xs[i]);
+            double ex = normalizeLong.applyAsDouble(xs[i + 1]);
+
+            // optimization: see if the rectangle is outside of the "bounding box" of the polyline at all
+            // if not, don't waste our time trying more complicated stuff
+            boolean outside = (dy < minY && ey < minY) || (dy > maxY && ey > maxY) || (dx < minX && ex < minX) || (dx > maxX && ex > maxX);
+
+            if (outside == false) {
+                if (includeBoundary) {
+                    if (lineCrossesLineWithBoundary(dx, dy, ex, ey, ax, ay, bx, by)
+                        || lineCrossesLineWithBoundary(dx, dy, ex, ey, bx, by, cx, cy)
+                        || lineCrossesLineWithBoundary(dx, dy, ex, ey, cx, cy, ax, ay)) {
+                        return true;
+                    }
+                } else {
+                    if (lineCrossesLine(dx, dy, ex, ey, ax, ay, bx, by)
+                        || lineCrossesLine(dx, dy, ex, ey, bx, by, cx, cy)
+                        || lineCrossesLine(dx, dy, ex, ey, cx, cy, ax, ay)) {
+                        return true;
+                    }
+                }
+            }
+
+        }
+        return false;
+    }
+
+    /** Checks if a rectangle crosses a h3 bin.*/
+    public static boolean crossesBox(
+        double[] xs,
+        double[] ys,
+        int numPoints,
+        boolean crossesDateline,
+        double minX,
+        double maxX,
+        double minY,
+        double maxY,
+        boolean includeBoundary
+    ) {
+        if (crossesDateline) {
+            return crossesBox(xs, ys, numPoints, minX, maxX, minY, maxY, includeBoundary, NORMALIZE_LONG_POS)
+                || crossesBox(xs, ys, numPoints, minX, maxX, minY, maxY, includeBoundary, NORMALIZE_LONG_NEG);
+        } else {
+            return crossesBox(xs, ys, numPoints, minX, maxX, minY, maxY, includeBoundary, DoubleUnaryOperator.identity());
+        }
+    }
+
+    private static boolean crossesBox(
+        double[] xs,
+        double[] ys,
+        int numPoints,
+        double minX,
+        double maxX,
+        double minY,
+        double maxY,
+        boolean includeBoundary,
+        DoubleUnaryOperator normalizeLong
+    ) {
+        // we just have to cross one edge to answer the question, so we descend the tree and return when
+        // we do.
+        for (int i = 0; i < numPoints - 1; i++) {
+            // we compute line intersections of every polygon edge with every box line.
+            // if we find one, return true.
+            // for each box line (AB):
+            // for each poly line (CD):
+            // intersects = orient(C,D,A) * orient(C,D,B) <= 0 && orient(A,B,C) * orient(A,B,D) <= 0
+            double cy = ys[i];
+            double dy = ys[i + 1];
+            double cx = normalizeLong.applyAsDouble(xs[i]);
+            double dx = normalizeLong.applyAsDouble(xs[i + 1]);
+
+            // optimization: see if either end of the line segment is contained by the rectangle
+            if (Rectangle.containsPoint(cy, cx, minY, maxY, minX, maxX) || Rectangle.containsPoint(dy, dx, minY, maxY, minX, maxX)) {
+                return true;
+            }
+
+            // optimization: see if the rectangle is outside of the "bounding box" of the polyline at all
+            // if not, don't waste our time trying more complicated stuff
+            boolean outside = (cy < minY && dy < minY) || (cy > maxY && dy > maxY) || (cx < minX && dx < minX) || (cx > maxX && dx > maxX);
+
+            if (outside == false) {
+                if (includeBoundary) {
+                    if (lineCrossesLineWithBoundary(cx, cy, dx, dy, minX, minY, maxX, minY)
+                        || lineCrossesLineWithBoundary(cx, cy, dx, dy, maxX, minY, maxX, maxY)
+                        || lineCrossesLineWithBoundary(cx, cy, dx, dy, maxX, maxY, minX, maxY)
+                        || lineCrossesLineWithBoundary(cx, cy, dx, dy, minX, maxY, minX, minY)) {
+                        // include boundaries: ensures box edges that terminate on the polygon are included
+                        return true;
+                    }
+                } else {
+                    if (lineCrossesLine(cx, cy, dx, dy, minX, minY, maxX, minY)
+                        || lineCrossesLine(cx, cy, dx, dy, maxX, minY, maxX, maxY)
+                        || lineCrossesLine(cx, cy, dx, dy, maxX, maxY, minX, maxY)
+                        || lineCrossesLine(cx, cy, dx, dy, minX, maxY, minX, minY)) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+}

+ 1 - 1
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/ShapeValues.java

@@ -155,7 +155,7 @@ public abstract class ShapeValues<T extends ShapeValues.ShapeValue> {
          * simple geometries, therefore it will fail if the LatLonGeometry is a {@link org.apache.lucene.geo.Rectangle}
          * that crosses the dateline.
          */
-        protected GeoRelation relate(Component2D component2D) throws IOException {
+        public GeoRelation relate(Component2D component2D) throws IOException {
             component2DRelationVisitor.reset(component2D);
             reader.visit(component2DRelationVisitor);
             return component2DRelationVisitor.relation();

+ 15 - 5
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/GeoGridQueryBuilder.java

@@ -8,6 +8,7 @@
 package org.elasticsearch.xpack.spatial.index.query;
 
 import org.apache.lucene.geo.GeoEncodingUtils;
+import org.apache.lucene.geo.LatLonGeometry;
 import org.apache.lucene.search.MatchNoDocsQuery;
 import org.apache.lucene.search.Query;
 import org.elasticsearch.ElasticsearchParseException;
@@ -30,6 +31,8 @@ import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils;
 import org.elasticsearch.xcontent.ParseField;
 import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentParser;
+import org.elasticsearch.xpack.spatial.common.H3CartesianUtil;
+import org.elasticsearch.xpack.spatial.index.mapper.GeoShapeWithDocValuesFieldMapper;
 
 import java.io.IOException;
 import java.util.Objects;
@@ -102,11 +105,18 @@ public class GeoGridQueryBuilder extends AbstractQueryBuilder<GeoGridQueryBuilde
 
             @Override
             protected Query toQuery(SearchExecutionContext context, String fieldName, MappedFieldType fieldType, String id) {
-                H3LatLonGeometry geometry = new H3LatLonGeometry(id);
-                if (fieldType instanceof GeoPointFieldMapper.GeoPointFieldType pointFieldType) {
-                    return pointFieldType.geoShapeQuery(context, fieldName, ShapeRelation.INTERSECTS, geometry);
-                } else if (fieldType instanceof GeoPointScriptFieldType scriptType) {
-                    return scriptType.geoShapeQuery(context, fieldName, ShapeRelation.INTERSECTS, geometry);
+                if (fieldType instanceof GeoShapeWithDocValuesFieldMapper.GeoShapeWithDocValuesFieldType geoShapeFieldType) {
+                    // shapes are solved on the cartesian geometry
+                    final LatLonGeometry geometry = H3CartesianUtil.getLatLonGeometry(H3.stringToH3(id));
+                    return geoShapeFieldType.geoShapeQuery(context, fieldName, ShapeRelation.INTERSECTS, geometry);
+                } else {
+                    // points are solved on the spherical geometry
+                    final H3LatLonGeometry geometry = new H3LatLonGeometry(id);
+                    if (fieldType instanceof GeoPointFieldMapper.GeoPointFieldType pointFieldType) {
+                        return pointFieldType.geoShapeQuery(context, fieldName, ShapeRelation.INTERSECTS, geometry);
+                    } else if (fieldType instanceof GeoPointScriptFieldType scriptType) {
+                        return scriptType.geoShapeQuery(context, fieldName, ShapeRelation.INTERSECTS, geometry);
+                    }
                 }
                 throw new QueryShardException(
                     context,

+ 191 - 0
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/AbstractGeoHexGridTiler.java

@@ -0,0 +1,191 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid;
+
+import org.elasticsearch.h3.H3;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoRelation;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoShapeValues;
+
+import java.io.IOException;
+
+/**
+ * Implements most of the logic for the GeoHex aggregation.
+ */
+abstract class AbstractGeoHexGridTiler extends GeoGridTiler {
+
+    private static final long[] RES0CELLS = H3.getLongRes0Cells();
+
+    AbstractGeoHexGridTiler(int precision) {
+        super(precision);
+    }
+
+    /** check if the provided H3 bin is in the solution space of this tiler */
+    protected abstract boolean h3IntersectsBounds(long h3);
+
+    /** Return the relation between the H3 bin and the geoValue. If the h3 is out of the tiler solution (e.g.
+     * {@link #h3IntersectsBounds(long)} is false), it should return {@link GeoRelation#QUERY_DISJOINT}
+    */
+    protected abstract GeoRelation relateTile(GeoShapeValues.GeoShapeValue geoValue, long h3) throws IOException;
+
+    /** Return true if the provided {@link GeoShapeValues.GeoShapeValue} is fully contained in our solution space.
+     */
+    protected abstract boolean valueInsideBounds(GeoShapeValues.GeoShapeValue geoValue) throws IOException;
+
+    @Override
+    public long encode(double x, double y) {
+        // TODO: maybe we should remove this method from the API
+        throw new IllegalArgumentException("no supported");
+    }
+
+    @Override
+    public int setValues(GeoShapeCellValues values, GeoShapeValues.GeoShapeValue geoValue) throws IOException {
+        final GeoShapeValues.BoundingBox bounds = geoValue.boundingBox();
+        assert bounds.minX() <= bounds.maxX();
+        // first check if we are touching just fetch cells
+        if (bounds.maxX() - bounds.minX() < 180d) {
+            final long minH3 = H3.geoToH3(bounds.minY(), bounds.minX(), precision);
+            final long maxH3 = H3.geoToH3(bounds.maxY(), bounds.maxX(), precision);
+            if (minH3 == maxH3) {
+                return setValuesFromPointResolution(minH3, values, geoValue);
+            }
+            // TODO: specialize when they are neighbour cells.
+        }
+        // recurse tree
+        return setValuesByRecursion(values, geoValue, bounds);
+    }
+
+    /**
+     * It calls {@link #maybeAdd(long, GeoRelation, GeoShapeCellValues, int)} for {@code h3} and the neighbour cells if necessary.
+     */
+    private int setValuesFromPointResolution(long h3, GeoShapeCellValues values, GeoShapeValues.GeoShapeValue geoValue) throws IOException {
+        int valueIndex = 0;
+        {
+            final GeoRelation relation = relateTile(geoValue, h3);
+            valueIndex = maybeAdd(h3, relation, values, valueIndex);
+            if (relation == GeoRelation.QUERY_CONTAINS) {
+                return valueIndex;
+            }
+        }
+        // Point resolution is done using H3 library which uses spherical geometry. It might happen that in cartesian, the
+        // actual point value is in a neighbour cell as well.
+        {
+            for (long n : H3.hexRing(h3)) {
+                final GeoRelation relation = relateTile(geoValue, n);
+                valueIndex = maybeAdd(n, relation, values, valueIndex);
+                if (relation == GeoRelation.QUERY_CONTAINS) {
+                    return valueIndex;
+                }
+            }
+        }
+        return valueIndex;
+    }
+
+    /**
+     * Adds {@code h3} to {@link GeoShapeCellValues} if {@link #relateTile(GeoShapeValues.GeoShapeValue, long)} returns
+     * a relation different to {@link GeoRelation#QUERY_DISJOINT}.
+     */
+    private int maybeAdd(long h3, GeoRelation relation, GeoShapeCellValues values, int valueIndex) {
+        if (relation != GeoRelation.QUERY_DISJOINT) {
+            values.resizeCell(valueIndex + 1);
+            values.add(valueIndex++, h3);
+        }
+        return valueIndex;
+    }
+
+    /**
+     * Recursively search the H3 tree, only following branches that intersect the geometry.
+     * Once at the required depth, then all cells that intersect are added to the collection.
+     */
+    // package private for testing
+    int setValuesByRecursion(GeoShapeCellValues values, GeoShapeValues.GeoShapeValue geoValue, GeoShapeValues.BoundingBox bounds)
+        throws IOException {
+        // NOTE: When we recurse, we cannot shortcut for CONTAINS relationship because it might fail when visiting noChilds.
+        int valueIndex = 0;
+        if (bounds.maxX() - bounds.minX() < 180d) {
+            final long minH3 = H3.geoToH3(bounds.minY(), bounds.minX(), 0);
+            final long maxH3 = H3.geoToH3(bounds.maxY(), bounds.maxX(), 0);
+            if (minH3 == maxH3) {
+                valueIndex = setValuesByRecursion(values, geoValue, minH3, 0, valueIndex);
+                for (long n : H3.hexRing(minH3)) {
+                    valueIndex = setValuesByRecursion(values, geoValue, n, 0, valueIndex);
+                }
+                return valueIndex;
+            }
+            // TODO: specialize when they are neighbour cells.
+        }
+        for (long h3 : RES0CELLS) {
+            valueIndex = setValuesByRecursion(values, geoValue, h3, 0, valueIndex);
+        }
+        return valueIndex;
+    }
+
+    /**
+     * Recursively search the H3 tree, only following branches that intersect the geometry.
+     * Once at the required depth, then all cells that intersect are added to the collection.
+     */
+    private int setValuesByRecursion(
+        GeoShapeCellValues values,
+        GeoShapeValues.GeoShapeValue geoValue,
+        long h3,
+        int precision,
+        int valueIndex
+    ) throws IOException {
+        assert H3.getResolution(h3) == precision;
+        final GeoRelation relation = relateTile(geoValue, h3);
+        if (precision == this.precision) {
+            // When we're at the desired level
+            return maybeAdd(h3, relation, values, valueIndex);
+        } else {
+            assert precision < this.precision;
+            // When we're at higher tree levels, check if we want to keep iterating.
+            if (relation != GeoRelation.QUERY_DISJOINT) {
+                int i = 0;
+                if (relation == GeoRelation.QUERY_INSIDE) {
+                    // H3 cells do not fully contain the children. The only one we know we fully contain
+                    // is the center child which is always at position 0.
+                    final long centerChild = H3.childPosToH3(h3, i++);
+                    valueIndex = setAllValuesByRecursion(values, centerChild, precision + 1, valueIndex, valueInsideBounds(geoValue));
+                }
+                final int numChildren = H3.h3ToChildrenSize(h3);
+                for (; i < numChildren; i++) {
+                    final long child = H3.childPosToH3(h3, i);
+                    valueIndex = setValuesByRecursion(values, geoValue, child, precision + 1, valueIndex);
+                }
+                // H3 cells do intersects with other cells that are not part of the children cells. If the parent cell of those
+                // cells is disjoint, they will not be visited, therefore visit them here.
+                final int numNoChildren = H3.h3ToNotIntersectingChildrenSize(h3);
+                for (int j = 0; j < numNoChildren; j++) {
+                    final long noChild = H3.noChildIntersectingPosToH3(h3, j);
+                    if (relateTile(geoValue, H3.h3ToParent(noChild)) == GeoRelation.QUERY_DISJOINT) {
+                        valueIndex = setValuesByRecursion(values, geoValue, noChild, precision + 1, valueIndex);
+                    }
+                }
+            }
+        }
+        return valueIndex;
+    }
+
+    /**
+     * Recursively scan the H3 tree, assuming all children are fully contained in the geometry.
+     * Once at the required depth, then all cells that intersect are added to the collection.
+     */
+    private int setAllValuesByRecursion(GeoShapeCellValues values, long h3, int precision, int valueIndex, boolean valueInsideBounds) {
+        if (valueInsideBounds || h3IntersectsBounds(h3)) {
+            if (precision == this.precision) {
+                values.resizeCell(valueIndex + 1);
+                values.add(valueIndex++, h3);
+            } else {
+                final int numChildren = H3.h3ToChildrenSize(h3);
+                for (int i = 0; i < numChildren; i++) {
+                    valueIndex = setAllValuesByRecursion(values, H3.childPosToH3(h3, i), precision + 1, valueIndex, valueInsideBounds);
+                }
+            }
+        }
+        return valueIndex;
+    }
+}

+ 146 - 0
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/BoundedGeoHexGridTiler.java

@@ -0,0 +1,146 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid;
+
+import org.elasticsearch.common.geo.GeoBoundingBox;
+import org.elasticsearch.common.geo.GeoPoint;
+import org.elasticsearch.common.geo.GeoUtils;
+import org.elasticsearch.geometry.Rectangle;
+import org.elasticsearch.h3.H3;
+import org.elasticsearch.xpack.spatial.common.H3CartesianUtil;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoRelation;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoShapeValues;
+
+import java.io.IOException;
+
+/**
+ * Bounded geohex aggregation. It accepts H3 addresses that intersect the provided bounds.
+ * The additional support for testing intersection with inflated bounds is used when testing
+ * parent cells, since child cells can exceed the bounds of their parent. We inflate the bounds
+ * by half of the width and half of the height.
+ */
+public class BoundedGeoHexGridTiler extends AbstractGeoHexGridTiler {
+    private final GeoBoundingBox inflatedBbox;
+    private final GeoBoundingBox bbox;
+    private final GeoHexVisitor visitor;
+    private final int precision;
+    private static final double FACTOR = 0.06;
+
+    public BoundedGeoHexGridTiler(int precision, GeoBoundingBox bbox) {
+        super(precision);
+        this.bbox = bbox;
+        this.visitor = new GeoHexVisitor();
+        this.precision = precision;
+        inflatedBbox = inflateBbox(precision, bbox);
+    }
+
+    private static GeoBoundingBox inflateBbox(int precision, GeoBoundingBox bbox) {
+        /*
+        * Here is the tricky part of this approach. We need to be able to filter cells at higher precisions
+        * because they are not in bounds, but we need to make sure we don't filter too much. We use h3 bins at the given
+        * resolution to check the height ands width at that level, and we factor it depending on the precision.
+        *
+        * The values have been tune using test GeoHexTilerTests#testLargeShapeWithBounds
+        */
+        final double factor = FACTOR * (1 << precision);
+        final Rectangle minMin = H3CartesianUtil.toBoundingBox(H3.geoToH3(bbox.bottom(), bbox.left(), precision));
+        final Rectangle maxMax = H3CartesianUtil.toBoundingBox(H3.geoToH3(bbox.top(), bbox.right(), precision));
+        // compute height and width at the given precision
+        final double height = Math.max(height(minMin), height(maxMax));
+        final double width = Math.max(width(minMin), width(maxMax));
+        // inflate the coordinates using the factor
+        final double minY = Math.max(bbox.bottom() - factor * height, -90d);
+        final double maxY = Math.min(bbox.top() + factor * height, 90d);
+        final double left = GeoUtils.normalizeLon(bbox.left() - factor * width);
+        final double right = GeoUtils.normalizeLon(bbox.right() + factor * width);
+        if (2 * factor * width + width(bbox) >= 360d) {
+            // if the total width bigger than the world, then it covers all longitude range.
+            return new GeoBoundingBox(new GeoPoint(maxY, -180d), new GeoPoint(minY, 180d));
+        } else {
+            return new GeoBoundingBox(new GeoPoint(maxY, left), new GeoPoint(minY, right));
+        }
+    }
+
+    private static double height(Rectangle rectangle) {
+        return rectangle.getMaxY() - rectangle.getMinY();
+    }
+
+    private static double width(Rectangle rectangle) {
+        if (rectangle.getMinX() > rectangle.getMaxX()) {
+            return 360d + rectangle.getMaxX() - rectangle.getMinX();
+        } else {
+            return rectangle.getMaxX() - rectangle.getMinX();
+        }
+    }
+
+    private static double width(GeoBoundingBox bbox) {
+        if (bbox.left() > bbox.right()) {
+            return 360d + bbox.right() - bbox.left();
+        } else {
+            return bbox.right() - bbox.left();
+        }
+    }
+
+    @Override
+    protected long getMaxCells() {
+        // TODO: Calculate correctly based on bounds
+        return UnboundedGeoHexGridTiler.calcMaxAddresses(precision);
+    }
+
+    @Override
+    protected boolean h3IntersectsBounds(long h3) {
+        visitor.reset(h3);
+        final int resolution = H3.getResolution(h3);
+        if (resolution != precision) {
+            return cellIntersectsBounds(visitor, inflatedBbox);
+        }
+        return cellIntersectsBounds(visitor, bbox);
+    }
+
+    @Override
+    protected GeoRelation relateTile(GeoShapeValues.GeoShapeValue geoValue, long h3) throws IOException {
+        visitor.reset(h3);
+        final int resolution = H3.getResolution(h3);
+        if (resolution != precision) {
+            if (cellIntersectsBounds(visitor, inflatedBbox)) {
+                // close to the poles, the properties of the H3 grid are lost because of the equirectangular projection,
+                // therefore we cannot ensure that the relationship at this level make any sense in the next level.
+                // Therefore, we just return CROSSES which just mean keep recursing.
+                if (visitor.getMaxY() > H3CartesianUtil.getNorthPolarBound(resolution)
+                    || visitor.getMinY() < H3CartesianUtil.getSouthPolarBound(resolution)) {
+                    return GeoRelation.QUERY_CROSSES;
+                }
+                geoValue.visit(visitor);
+                return visitor.relation();
+            } else {
+                return GeoRelation.QUERY_DISJOINT;
+            }
+        }
+        if (cellIntersectsBounds(visitor, bbox)) {
+            geoValue.visit(visitor);
+            return visitor.relation();
+        }
+        return GeoRelation.QUERY_DISJOINT;
+    }
+
+    @Override
+    protected boolean valueInsideBounds(GeoShapeValues.GeoShapeValue geoValue) {
+        if (bbox.bottom() <= geoValue.boundingBox().minY() && bbox.top() >= geoValue.boundingBox().maxY()) {
+            if (bbox.right() < bbox.left()) {
+                return bbox.left() <= geoValue.boundingBox().minX() || bbox.right() >= geoValue.boundingBox().maxX();
+            } else {
+                return bbox.left() <= geoValue.boundingBox().minX() && bbox.right() >= geoValue.boundingBox().maxX();
+            }
+        }
+        return false;
+    }
+
+    private static boolean cellIntersectsBounds(GeoHexVisitor visitor, GeoBoundingBox bbox) {
+        return visitor.intersectsBbox(bbox.left(), bbox.right(), bbox.bottom(), bbox.top());
+    }
+}

+ 353 - 0
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoHexVisitor.java

@@ -0,0 +1,353 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid;
+
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.GeoUtils;
+import org.apache.lucene.util.ArrayUtil;
+import org.elasticsearch.xpack.spatial.common.H3CartesianUtil;
+import org.elasticsearch.xpack.spatial.index.fielddata.CoordinateEncoder;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoRelation;
+import org.elasticsearch.xpack.spatial.index.fielddata.TriangleTreeVisitor;
+
+/**
+ * A reusable tree reader visitor for a previous serialized {@link org.elasticsearch.geometry.Geometry}.
+ *
+ * This class supports checking H3 cells relations against a serialized triangle tree. It has the special property that if
+ * a geometry touches any of the edges, then it will never return {@link GeoRelation#QUERY_CONTAINS}, for example a point on the boundary
+ * return {@link GeoRelation#QUERY_CROSSES}.
+ */
+class GeoHexVisitor extends TriangleTreeVisitor.TriangleTreeDecodedVisitor {
+
+    private GeoRelation relation;
+    private final double[] xs, ys;
+    private double minX, maxX, minY, maxY;
+    private boolean crossesDateline;
+    private int numPoints;
+
+    GeoHexVisitor() {
+        super(CoordinateEncoder.GEO);
+        xs = new double[H3CartesianUtil.MAX_ARRAY_SIZE];
+        ys = new double[H3CartesianUtil.MAX_ARRAY_SIZE];
+    }
+
+    public double[] getXs() {
+        return ArrayUtil.copyOfSubArray(xs, 0, numPoints);
+    }
+
+    public double[] getYs() {
+        return ArrayUtil.copyOfSubArray(ys, 0, numPoints);
+    }
+
+    public double getLeftX() {
+        return crossesDateline ? maxX : minX;
+    }
+
+    public double getRightX() {
+        return crossesDateline ? minX : maxX;
+    }
+
+    public double getMinY() {
+        return minY;
+    }
+
+    public double getMaxY() {
+        return maxY;
+    }
+
+    /**
+     * reset this visitor to the provided h3 cell
+     */
+    public void reset(long h3) {
+        numPoints = H3CartesianUtil.computePoints(h3, xs, ys);
+        double minX = Double.POSITIVE_INFINITY;
+        double maxX = Double.NEGATIVE_INFINITY;
+        double minY = Double.POSITIVE_INFINITY;
+        double maxY = Double.NEGATIVE_INFINITY;
+        for (int i = 0; i < numPoints; i++) {
+            minX = Math.min(minX, xs[i]);
+            maxX = Math.max(maxX, xs[i]);
+            minY = Math.min(minY, ys[i]);
+            maxY = Math.max(maxY, ys[i]);
+        }
+        this.minX = minX;
+        this.maxX = maxX;
+        this.minY = minY;
+        this.maxY = maxY;
+        this.crossesDateline = maxX - minX > 180d && H3CartesianUtil.isPolar(h3) == false;
+    }
+
+    /**
+     * return the computed relation.
+     */
+    public GeoRelation relation() {
+        return relation;
+    }
+
+    @Override
+    public void visitDecodedPoint(double x, double y) {
+        updateRelation(relatePoint(x, y));
+    }
+
+    @Override
+    protected void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) {
+        updateRelation(relateLine(aX, aY, bX, bY));
+    }
+
+    @Override
+    protected void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) {
+        final boolean ab = (metadata & 1 << 4) == 1 << 4;
+        final boolean bc = (metadata & 1 << 5) == 1 << 5;
+        final boolean ca = (metadata & 1 << 6) == 1 << 6;
+        updateRelation(relateTriangle(aX, aY, ab, bX, bY, bc, cX, cY, ca));
+    }
+
+    private void updateRelation(GeoRelation relation) {
+        if (relation != GeoRelation.QUERY_DISJOINT) {
+            if (relation == GeoRelation.QUERY_INSIDE && canBeInside()) {
+                this.relation = GeoRelation.QUERY_INSIDE;
+            } else if (relation == GeoRelation.QUERY_CONTAINS && canBeContained()) {
+                this.relation = GeoRelation.QUERY_CONTAINS;
+            } else {
+                this.relation = GeoRelation.QUERY_CROSSES;
+            }
+        } else {
+            adjustRelationForNotIntersectingComponent();
+        }
+    }
+
+    private void adjustRelationForNotIntersectingComponent() {
+        if (relation == null) {
+            this.relation = GeoRelation.QUERY_DISJOINT;
+        } else if (relation == GeoRelation.QUERY_CONTAINS) {
+            this.relation = GeoRelation.QUERY_CROSSES;
+        }
+    }
+
+    private boolean canBeContained() {
+        return this.relation == null || this.relation == GeoRelation.QUERY_CONTAINS;
+    }
+
+    private boolean canBeInside() {
+        return this.relation != GeoRelation.QUERY_CONTAINS;
+    }
+
+    @Override
+    public boolean push() {
+        return this.relation != GeoRelation.QUERY_CROSSES;
+    }
+
+    @Override
+    public boolean pushDecodedX(double minX) {
+        if (crossesDateline || this.maxX >= minX) {
+            return true;
+        }
+        adjustRelationForNotIntersectingComponent();
+        return false;
+    }
+
+    @Override
+    public boolean pushDecodedY(double minY) {
+        if (this.maxY >= minY) {
+            return true;
+        }
+        adjustRelationForNotIntersectingComponent();
+        return false;
+    }
+
+    @Override
+    public boolean pushDecoded(double maxX, double maxY) {
+        if (this.minY <= maxY && (this.crossesDateline || minX <= maxX)) {
+            return true;
+        }
+        adjustRelationForNotIntersectingComponent();
+        return false;
+    }
+
+    @Override
+    @SuppressWarnings("HiddenField")
+    public boolean pushDecoded(double minX, double minY, double maxX, double maxY) {
+        if (boxesAreDisjoint(minX, maxX, minY, maxY)) {
+            // shapes are disjoint
+            this.relation = GeoRelation.QUERY_DISJOINT;
+            return false;
+        }
+        relation = null;
+        return true;
+    }
+
+    /** Check if the provided bounding box intersect the H3 bin. It supports bounding boxes
+     * crossing the dateline. */
+    public boolean intersectsBbox(double minX, double maxX, double minY, double maxY) {
+        if (minX > maxX) {
+            return relateBbox(minX, GeoUtils.MAX_LON_INCL, minY, maxY) || relateBbox(GeoUtils.MIN_LON_INCL, maxX, minY, maxY);
+        } else {
+            return relateBbox(minX, maxX, minY, maxY);
+        }
+    }
+
+    private boolean relateBbox(double minX, double maxX, double minY, double maxY) {
+        if (boxesAreDisjoint(minX, maxX, minY, maxY)) {
+            return false;
+        }
+        if (minX <= xs[0] && maxX >= xs[0] && minY <= ys[0] && maxY >= ys[0]) {
+            return true;
+        }
+        return relatePoint(minX, minY) != GeoRelation.QUERY_DISJOINT
+            || H3CartesianUtil.crossesBox(xs, ys, numPoints, crossesDateline, minX, maxX, minY, maxY, true);
+    }
+
+    /**
+     * Checks if the rectangle contains the provided point
+     **/
+    private GeoRelation relatePoint(double x, double y) {
+        if (boxesAreDisjoint(x, x, y, y)) {
+            return GeoRelation.QUERY_DISJOINT;
+        }
+        return H3CartesianUtil.relatePoint(xs, ys, numPoints, crossesDateline, x, y);
+    }
+
+    /**
+     * Compute the relationship between the provided line and this h3 bin
+     **/
+    private GeoRelation relateLine(double aX, double aY, double bX, double bY) {
+        // query contains any points
+        GeoRelation relation1 = relatePoint(aX, aY);
+        GeoRelation relation2 = relatePoint(bX, bY);
+        if (relation1 != relation2 || relation1 == GeoRelation.QUERY_CROSSES) {
+            return GeoRelation.QUERY_CROSSES;
+        } else if (relation1 == GeoRelation.QUERY_CONTAINS) {
+            if (crossesDateline) {
+                final double minX = Math.min(aX, bX);
+                final double maxX = Math.max(aX, bX);
+                final double minY = Math.min(aY, bY);
+                final double maxY = Math.max(aY, bY);
+                if (H3CartesianUtil.crossesLine(xs, ys, numPoints, crossesDateline, minX, maxX, minY, maxY, aX, aY, bX, bY, true)) {
+                    return GeoRelation.QUERY_CROSSES;
+                }
+            }
+            return GeoRelation.QUERY_CONTAINS;
+        }
+        // 2. check crossings
+        if (edgeIntersectsQuery(aX, aY, bX, bY, true)) {
+            return GeoRelation.QUERY_CROSSES;
+        }
+        return GeoRelation.QUERY_DISJOINT;
+    }
+
+    /**
+     * Compute the relationship between the provided triangle and this h3 bin
+     **/
+    private GeoRelation relateTriangle(
+        double aX,
+        double aY,
+        boolean ab,
+        double bX,
+        double bY,
+        boolean bc,
+        double cX,
+        double cY,
+        boolean ca
+    ) {
+        // compute bounding box of triangle
+        double tMinX = StrictMath.min(StrictMath.min(aX, bX), cX);
+        double tMaxX = StrictMath.max(StrictMath.max(aX, bX), cX);
+        double tMinY = StrictMath.min(StrictMath.min(aY, bY), cY);
+        double tMaxY = StrictMath.max(StrictMath.max(aY, bY), cY);
+
+        // 1. check bounding boxes are disjoint
+        if (boxesAreDisjoint(tMinX, tMaxX, tMinY, tMaxY)) {
+            return GeoRelation.QUERY_DISJOINT;
+        }
+
+        GeoRelation relation1 = relatePoint(aX, aY);
+        GeoRelation relation2 = relatePoint(bX, bY);
+        GeoRelation relation3 = relatePoint(cX, cY);
+
+        // 2. query contains any triangle points
+        if (relation1 != relation2 || relation1 != relation3 || relation1 == GeoRelation.QUERY_CROSSES) {
+            return GeoRelation.QUERY_CROSSES;
+        } else if (relation1 == GeoRelation.QUERY_CONTAINS) {
+            if (crossesDateline
+                && H3CartesianUtil.crossesTriangle(
+                    xs,
+                    ys,
+                    numPoints,
+                    crossesDateline,
+                    tMinX,
+                    tMaxX,
+                    tMinY,
+                    tMaxY,
+                    aX,
+                    aY,
+                    bX,
+                    bY,
+                    cX,
+                    cY,
+                    true
+                )) {
+                return GeoRelation.QUERY_CROSSES;
+            }
+            return GeoRelation.QUERY_CONTAINS;
+        }
+
+        boolean within = false;
+        if (edgeIntersectsQuery(aX, aY, bX, bY, false)) {
+            if (ab) {
+                return GeoRelation.QUERY_CROSSES;
+            }
+            within = true;
+        }
+
+        if (edgeIntersectsQuery(bX, bY, cX, cY, false)) {
+            if (bc) {
+                return GeoRelation.QUERY_CROSSES;
+            }
+            within = true;
+        }
+
+        if (edgeIntersectsQuery(cX, cY, aX, aY, false)) {
+            if (ca) {
+                return GeoRelation.QUERY_CROSSES;
+            }
+            within = true;
+        }
+
+        if (within || Component2D.pointInTriangle(tMinX, tMaxX, tMinY, tMaxY, xs[0], ys[0], aX, aY, bX, bY, cX, cY)) {
+            return GeoRelation.QUERY_INSIDE;
+        }
+
+        return GeoRelation.QUERY_DISJOINT;
+    }
+
+    /**
+     * returns true if the edge (defined by (ax, ay) (bx, by)) intersects the query
+     */
+    private boolean edgeIntersectsQuery(double ax, double ay, double bx, double by, boolean includeBoundary) {
+        final double minX = Math.min(ax, bx);
+        final double maxX = Math.max(ax, bx);
+        final double minY = Math.min(ay, by);
+        final double maxY = Math.max(ay, by);
+        return boxesAreDisjoint(minX, maxX, minY, maxY) == false
+            && H3CartesianUtil.crossesLine(xs, ys, numPoints, crossesDateline, minX, maxX, minY, maxY, ax, ay, bx, by, includeBoundary);
+    }
+
+    /**
+     * utility method to check if two boxes are disjoint
+     */
+    private boolean boxesAreDisjoint(final double minX, final double maxX, final double minY, final double maxY) {
+        if ((maxY < this.minY || minY > this.maxY) == false) {
+            if (crossesDateline) {
+                return maxX < this.minX && minX > this.maxX;
+            } else {
+                return maxX < this.minX || minX > this.maxX;
+            }
+        }
+        return true;
+    }
+}

+ 43 - 0
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoShapeHexGridAggregator.java

@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid;
+
+import org.elasticsearch.search.aggregations.Aggregator;
+import org.elasticsearch.search.aggregations.AggregatorFactories;
+import org.elasticsearch.search.aggregations.CardinalityUpperBound;
+import org.elasticsearch.search.aggregations.support.AggregationContext;
+import org.elasticsearch.search.aggregations.support.ValuesSource;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class GeoShapeHexGridAggregator extends GeoHexGridAggregator {
+    public GeoShapeHexGridAggregator(
+        String name,
+        AggregatorFactories factories,
+        ValuesSource.Numeric valuesSource,
+        int requiredSize,
+        int shardSize,
+        AggregationContext context,
+        Aggregator parent,
+        CardinalityUpperBound cardinality,
+        Map<String, Object> metadata
+    ) throws IOException {
+        super(name, factories, valuesSource, requiredSize, shardSize, context, parent, cardinality, metadata);
+    }
+
+    /**
+     * This is a wrapper method to expose this protected method to {@link GeoShapeCellIdSource}
+     *
+     * @param bytes the number of bytes to register or negative to deregister the bytes
+     * @return the cumulative size in bytes allocated by this aggregator to service this request
+     */
+    public long addRequestBytes(long bytes) {
+        return addRequestCircuitBreakerBytes(bytes);
+    }
+}

+ 69 - 0
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/UnboundedGeoHexGridTiler.java

@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid;
+
+import org.elasticsearch.h3.H3;
+import org.elasticsearch.xpack.spatial.common.H3CartesianUtil;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoRelation;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoShapeValues;
+
+import java.io.IOException;
+
+/**
+ * Unbounded geohex aggregation. It accepts any hash.
+ */
+public class UnboundedGeoHexGridTiler extends AbstractGeoHexGridTiler {
+
+    private final long maxAddresses;
+
+    private final GeoHexVisitor visitor;
+
+    public UnboundedGeoHexGridTiler(int precision) {
+        super(precision);
+        this.visitor = new GeoHexVisitor();
+        maxAddresses = calcMaxAddresses(precision);
+    }
+
+    @Override
+    protected boolean h3IntersectsBounds(long h3) {
+        return true;
+    }
+
+    @Override
+    protected GeoRelation relateTile(GeoShapeValues.GeoShapeValue geoValue, long h3) throws IOException {
+        visitor.reset(h3);
+        final int resolution = H3.getResolution(h3);
+        if (resolution != precision
+            && (visitor.getMaxY() > H3CartesianUtil.getNorthPolarBound(resolution)
+                || visitor.getMinY() < H3CartesianUtil.getSouthPolarBound(resolution))) {
+            // close to the poles, the properties of the H3 grid are lost because of the equirectangular projection,
+            // therefore we cannot ensure that the relationship at this level make any sense in the next level.
+            // Therefore, we just return CROSSES which just mean keep recursing.
+            return GeoRelation.QUERY_CROSSES;
+        }
+        geoValue.visit(visitor);
+        return visitor.relation();
+    }
+
+    @Override
+    protected boolean valueInsideBounds(GeoShapeValues.GeoShapeValue geoValue) {
+        return true;
+    }
+
+    @Override
+    protected long getMaxCells() {
+        return maxAddresses;
+    }
+
+    public static long calcMaxAddresses(int precision) {
+        // TODO: Verify this (and perhaps move the calculation into H3 and based on NUM_BASE_CELLS and others)
+        final int baseHexagons = 110;
+        final int basePentagons = 12;
+        return baseHexagons * (long) Math.pow(7, precision) + basePentagons * (long) Math.pow(6, precision);
+    }
+}

+ 10 - 0
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/SpatialPluginTests.java

@@ -59,6 +59,16 @@ public class SpatialPluginTests extends ESTestCase {
         }, "geohex_grid", "geo_point");
     }
 
+    public void testGeoShapeHexLicenseCheck() {
+        checkLicenseRequired(GeoShapeValuesSourceType.instance(), GeoHexGridAggregationBuilder.REGISTRY_KEY, (agg) -> {
+            try {
+                agg.build(null, AggregatorFactories.EMPTY, null, 0, null, 0, 0, null, null, CardinalityUpperBound.NONE, null);
+            } catch (IOException e) {
+                fail("Unexpected exception: " + e.getMessage());
+            }
+        }, "geohex_grid", "geo_shape");
+    }
+
     public void testGeoGridLicenseCheck() {
         for (ValuesSourceRegistry.RegistryKey<GeoGridAggregatorSupplier> registryKey : Arrays.asList(
             GeoHashGridAggregationBuilder.REGISTRY_KEY,

+ 241 - 0
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/common/H3CartesianUtilTests.java

@@ -0,0 +1,241 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.spatial.common;
+
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.LatLonGeometry;
+import org.apache.lucene.tests.geo.GeoTestUtil;
+import org.apache.lucene.util.ArrayUtil;
+import org.elasticsearch.common.geo.GeometryNormalizer;
+import org.elasticsearch.common.geo.Orientation;
+import org.elasticsearch.geo.GeometryTestUtils;
+import org.elasticsearch.geometry.Geometry;
+import org.elasticsearch.geometry.LinearRing;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.Polygon;
+import org.elasticsearch.geometry.utils.WellKnownText;
+import org.elasticsearch.h3.H3;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoRelation;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoShapeValues;
+import org.elasticsearch.xpack.spatial.util.GeoTestUtils;
+import org.hamcrest.Matchers;
+
+import java.io.IOException;
+
+public class H3CartesianUtilTests extends ESTestCase {
+
+    public void testLevel1() throws IOException {
+        for (int i = 0; i < 10000; i++) {
+            Point point = GeometryTestUtils.randomPoint();
+            GeoShapeValues.GeoShapeValue geoValue = GeoTestUtils.geoShapeValue(point);
+            boolean inside = false;
+            for (long h3 : H3.getLongRes0Cells()) {
+                if (geoValue.relate(H3CartesianUtil.getLatLonGeometry(h3)) != GeoRelation.QUERY_DISJOINT) {
+                    inside = true;
+                    break;
+                }
+            }
+            if (inside == false) {
+                fail(
+                    "failing matching point: " + WellKnownText.toWKT(new org.elasticsearch.geometry.Point(point.getLon(), point.getLat()))
+                );
+            }
+        }
+    }
+
+    public void testLevel2() throws IOException {
+        for (int i = 0; i < 10000; i++) {
+            Point point = GeometryTestUtils.randomPoint();
+            GeoShapeValues.GeoShapeValue geoValue = GeoTestUtils.geoShapeValue(point);
+            boolean inside = false;
+            for (long res0Cell : H3.getLongRes0Cells()) {
+                for (long h3 : H3.h3ToChildren(res0Cell)) {
+                    if (geoValue.relate(H3CartesianUtil.getLatLonGeometry(h3)) != GeoRelation.QUERY_DISJOINT) {
+                        inside = true;
+                        break;
+                    }
+                }
+            }
+            if (inside == false) {
+                fail(
+                    "failing matching point: " + WellKnownText.toWKT(new org.elasticsearch.geometry.Point(point.getLon(), point.getLat()))
+                );
+            }
+        }
+    }
+
+    public void testNorthPole() throws IOException {
+        for (int res = 0; res <= H3.MAX_H3_RES; res++) {
+            final long h3 = H3.geoToH3(90, 0, res);
+            final LatLonGeometry latLonGeometry = H3CartesianUtil.getLatLonGeometry(h3);
+            final double lon = GeoTestUtil.nextLongitude();
+            {
+                GeoShapeValues.GeoShapeValue geoValue = GeoTestUtils.geoShapeValue(new Point(lon, 90));
+                assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CONTAINS));
+            }
+            {
+                final double bound = H3CartesianUtil.getNorthPolarBound(res);
+                final double lat = randomValueOtherThanMany(l -> l > bound, GeoTestUtil::nextLatitude);
+                GeoShapeValues.GeoShapeValue geoValue = GeoTestUtils.geoShapeValue(new Point(lon, lat));
+                assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_DISJOINT));
+            }
+        }
+    }
+
+    public void testSouthPole() throws IOException {
+        for (int res = 0; res <= H3.MAX_H3_RES; res++) {
+            final long h3 = H3.geoToH3(-90, 0, res);
+            final LatLonGeometry latLonGeometry = H3CartesianUtil.getLatLonGeometry(h3);
+            final double lon = GeoTestUtil.nextLongitude();
+            {
+                GeoShapeValues.GeoShapeValue geoValue = GeoTestUtils.geoShapeValue(new Point(lon, -90));
+                assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CONTAINS));
+            }
+            {
+                final double bound = H3CartesianUtil.getSouthPolarBound(res);
+                final double lat = randomValueOtherThanMany(l -> l < bound, GeoTestUtil::nextLatitude);
+                GeoShapeValues.GeoShapeValue geoValue = GeoTestUtils.geoShapeValue(new Point(lon, lat));
+                assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_DISJOINT));
+            }
+        }
+    }
+
+    public void testDateline() throws IOException {
+        final long h3 = H3.geoToH3(0, 180, 0);
+        final LatLonGeometry latLonGeometry = H3CartesianUtil.getLatLonGeometry(h3);
+        // points
+        {
+            GeoShapeValues.GeoShapeValue geoValue = GeoTestUtils.geoShapeValue(new org.elasticsearch.geometry.Point(0, 0));
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_DISJOINT));
+            geoValue = GeoTestUtils.geoShapeValue(new org.elasticsearch.geometry.Point(180, 0));
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CONTAINS));
+            geoValue = GeoTestUtils.geoShapeValue(new org.elasticsearch.geometry.Point(-180, 0));
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CONTAINS));
+            geoValue = GeoTestUtils.geoShapeValue(new org.elasticsearch.geometry.Point(179, 0));
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CONTAINS));
+            geoValue = GeoTestUtils.geoShapeValue(new org.elasticsearch.geometry.Point(-179, 0));
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CONTAINS));
+        }
+        // lines
+        {
+            GeoShapeValues.GeoShapeValue geoValue = GeoTestUtils.geoShapeValue(
+                new org.elasticsearch.geometry.Line(new double[] { 0, 0 }, new double[] { -1, 1 })
+            );
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_DISJOINT));
+            geoValue = GeoTestUtils.geoShapeValue(new org.elasticsearch.geometry.Line(new double[] { 180, 180 }, new double[] { -1, 1 }));
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CONTAINS));
+            geoValue = GeoTestUtils.geoShapeValue(new org.elasticsearch.geometry.Line(new double[] { -180, -180 }, new double[] { -1, 1 }));
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CONTAINS));
+            geoValue = GeoTestUtils.geoShapeValue(new org.elasticsearch.geometry.Line(new double[] { 179, 179 }, new double[] { -1, 1 }));
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CONTAINS));
+            geoValue = GeoTestUtils.geoShapeValue(new org.elasticsearch.geometry.Line(new double[] { -179, -179 }, new double[] { -1, 1 }));
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CONTAINS));
+            geoValue = GeoTestUtils.geoShapeValue(new org.elasticsearch.geometry.Line(new double[] { -179, 179 }, new double[] { -1, 1 }));
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CROSSES));
+        }
+        // polygons
+        {
+            GeoShapeValues.GeoShapeValue geoValue = GeoTestUtils.geoShapeValue(
+                new org.elasticsearch.geometry.Polygon(new LinearRing(new double[] { 0, 0, 1, 0 }, new double[] { -1, 1, 1, -1 }))
+            );
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_DISJOINT));
+            geoValue = GeoTestUtils.geoShapeValue(
+                new org.elasticsearch.geometry.Polygon(new LinearRing(new double[] { 180, 180, 179, 180 }, new double[] { -1, 1, 1, -1 }))
+            );
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CONTAINS));
+            geoValue = GeoTestUtils.geoShapeValue(
+                new org.elasticsearch.geometry.Polygon(
+                    new LinearRing(new double[] { -180, -180, -179, -180 }, new double[] { -1, 1, 1, -1 })
+                )
+            );
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CONTAINS));
+            geoValue = GeoTestUtils.geoShapeValue(
+                new org.elasticsearch.geometry.Polygon(new LinearRing(new double[] { 179, 179, 179.5, 179 }, new double[] { -1, 1, 1, -1 }))
+            );
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CONTAINS));
+            geoValue = GeoTestUtils.geoShapeValue(
+                new org.elasticsearch.geometry.Polygon(
+                    new LinearRing(new double[] { -179, -179, -179.5, -179 }, new double[] { -1, 1, 1, -1 })
+                )
+            );
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CONTAINS));
+            geoValue = GeoTestUtils.geoShapeValue(
+                new org.elasticsearch.geometry.Polygon(
+                    new LinearRing(new double[] { -179, 179, -178, -179 }, new double[] { -1, 1, 1, -1 })
+                )
+            );
+            assertThat(geoValue.relate(latLonGeometry), Matchers.equalTo(GeoRelation.QUERY_CROSSES));
+        }
+    }
+
+    public void testRandomBasic() throws IOException {
+        for (int res = 0; res < H3.MAX_H3_RES; res++) {
+            final long h3 = H3.geoToH3(0, 0, res);
+            final GeoShapeValues.GeoShapeValue geoValue = GeoTestUtils.geoShapeValue(getGeometry(h3));
+            final long[] children = H3.h3ToChildren(h3);
+            assertThat(geoValue.relate(getComponent(children[0])), Matchers.equalTo(GeoRelation.QUERY_INSIDE));
+            for (int i = 1; i < children.length; i++) {
+                assertThat(geoValue.relate(getComponent(children[i])), Matchers.equalTo(GeoRelation.QUERY_CROSSES));
+            }
+            for (long noChild : H3.h3ToNoChildrenIntersecting(h3)) {
+                assertThat(geoValue.relate(getComponent(noChild)), Matchers.equalTo(GeoRelation.QUERY_CROSSES));
+            }
+        }
+    }
+
+    public void testRandomDateline() throws IOException {
+        for (int res = 0; res < H3.MAX_H3_RES; res++) {
+            final long h3 = H3.geoToH3(0, 180, res);
+            final GeoShapeValues.GeoShapeValue geoValue = GeoTestUtils.geoShapeValue(getGeometry(h3));
+            final long[] children = H3.h3ToChildren(h3);
+            final Component2D component2D = getComponent(children[0]);
+            // this is a current limitation because we break polygons around the dateline.
+            final GeoRelation expected = component2D.getMaxX() - component2D.getMinX() == 360d
+                ? GeoRelation.QUERY_CROSSES
+                : GeoRelation.QUERY_INSIDE;
+            assertThat(geoValue.relate(component2D), Matchers.equalTo(expected));
+            for (int i = 1; i < children.length; i++) {
+                assertThat(geoValue.relate(getComponent(children[i])), Matchers.equalTo(GeoRelation.QUERY_CROSSES));
+            }
+            for (long noChild : H3.h3ToNoChildrenIntersecting(h3)) {
+                assertThat(geoValue.relate(getComponent(noChild)), Matchers.equalTo(GeoRelation.QUERY_CROSSES));
+            }
+        }
+    }
+
+    private static Component2D getComponent(long h3) {
+        return LatLonGeometry.create(H3CartesianUtil.getLatLonGeometry(h3));
+    }
+
+    private static Geometry getGeometry(long h3) {
+        final double[] xs = new double[H3CartesianUtil.MAX_ARRAY_SIZE];
+        final double[] ys = new double[H3CartesianUtil.MAX_ARRAY_SIZE];
+        final int numPoints = H3CartesianUtil.computePoints(h3, xs, ys);
+        final Polygon polygon = new Polygon(
+            new LinearRing(ArrayUtil.copyOfSubArray(xs, 0, numPoints), ArrayUtil.copyOfSubArray(ys, 0, numPoints))
+        );
+        double minX = Double.POSITIVE_INFINITY;
+        double maxX = Double.NEGATIVE_INFINITY;
+        for (int i = 0; i < numPoints; i++) {
+            minX = Math.min(minX, xs[i]);
+            maxX = Math.max(maxX, xs[i]);
+        }
+        if (maxX - minX > 180d && H3CartesianUtil.isPolar(h3) == false) {
+            final Geometry geometry = GeometryNormalizer.apply(Orientation.CCW, polygon);
+            if (geometry instanceof Polygon) {
+                // there is a bug on the code that breaks polygons across the dateline
+                // when polygon is close to the pole (I think) so we need to try again
+                return GeometryNormalizer.apply(Orientation.CW, polygon);
+            }
+            return geometry;
+        } else {
+            return polygon;
+        }
+    }
+}

+ 43 - 27
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoGridTilerTestCase.java

@@ -19,6 +19,7 @@ import org.elasticsearch.common.settings.ClusterSettings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.geo.GeometryTestUtils;
 import org.elasticsearch.geometry.Geometry;
+import org.elasticsearch.geometry.Line;
 import org.elasticsearch.geometry.LinearRing;
 import org.elasticsearch.geometry.MultiLine;
 import org.elasticsearch.geometry.MultiPolygon;
@@ -57,6 +58,11 @@ public abstract class GeoGridTilerTestCase extends ESTestCase {
 
     protected abstract Rectangle getCell(double lon, double lat, int precision);
 
+    /** Tilers that are not rectangular cannot run all tests, eg. H3 tiler */
+    protected boolean isRectangularTiler() {
+        return true;
+    }
+
     protected abstract long getCellsForDiffPrecision(int precisionDiff);
 
     protected abstract void assertSetValuesBruteAndRecursive(Geometry geometry) throws Exception;
@@ -85,6 +91,12 @@ public abstract class GeoGridTilerTestCase extends ESTestCase {
         }
     }
 
+    public void testGeoGridSetValuesBruteAndRecursiveLine() throws Exception {
+        Line geometry = GeometryTestUtils.randomLine(false);
+        assertSetValuesBruteAndRecursive(geometry);
+
+    }
+
     public void testGeoGridSetValuesBruteAndRecursiveMultiline() throws Exception {
         MultiLine geometry = GeometryTestUtils.randomMultiLine(false);
         assertSetValuesBruteAndRecursive(geometry);
@@ -95,8 +107,13 @@ public abstract class GeoGridTilerTestCase extends ESTestCase {
         assertSetValuesBruteAndRecursive(geometry);
     }
 
-    public void testGeoGridSetValuesBruteAndRecursivePoints() throws Exception {
-        Geometry geometry = randomBoolean() ? GeometryTestUtils.randomPoint(false) : GeometryTestUtils.randomMultiPoint(false);
+    public void testGeoGridSetValuesBruteAndRecursivePoint() throws Exception {
+        Geometry geometry = GeometryTestUtils.randomPoint(false);
+        assertSetValuesBruteAndRecursive(geometry);
+    }
+
+    public void testGeoGridSetValuesBruteAndRecursiveMultiPoint() throws Exception {
+        Geometry geometry = GeometryTestUtils.randomMultiPoint(false);
         assertSetValuesBruteAndRecursive(geometry);
     }
 
@@ -104,15 +121,10 @@ public abstract class GeoGridTilerTestCase extends ESTestCase {
     public void testGeoGridSetValuesBoundingBoxes_BoundedGeoShapeCellValues() throws Exception {
         for (int i = 0; i < 10; i++) {
             int precision = randomIntBetween(0, 3);
-            Geometry geometry = GeometryNormalizer.apply(Orientation.CCW, randomValueOtherThanMany(g -> {
-                try {
-                    // make sure is a valid shape
-                    new GeoShapeIndexer(Orientation.CCW, "test").indexShape(g);
-                    return false;
-                } catch (Exception e) {
-                    return true;
-                }
-            }, () -> boxToGeo(randomBBox())));
+            Geometry geometry = GeometryNormalizer.apply(
+                Orientation.CCW,
+                randomValueOtherThanMany(this::geometryIsInvalid, () -> boxToGeo(randomBBox()))
+            );
 
             GeoBoundingBox geoBoundingBox = randomValueOtherThanMany(b -> b.right() == -180 && b.left() == 180, () -> randomBBox());
             GeoShapeValues.GeoShapeValue value = geoShapeValue(geometry);
@@ -125,11 +137,11 @@ public abstract class GeoGridTilerTestCase extends ESTestCase {
             assertTrue(cellValues.advanceExact(0));
             int numBuckets = cellValues.docValueCount();
             int expected = expectedBuckets(value, precision, geoBoundingBox);
-            assertThat(numBuckets, equalTo(expected));
+            assertThat("[" + i + ":" + precision + "] bucket count", numBuckets, equalTo(expected));
         }
     }
 
-    // tests that bounding boxes that crosses the dateline and cover all longitude values are correctly wrapped
+    // tests that bounding boxes that cross the dateline and cover all longitude values are correctly wrapped
     public void testGeoGridSetValuesBoundingBoxes_coversAllLongitudeValues() throws Exception {
         int precision = 3;
         Geometry geometry = new Rectangle(-92, 180, 0.99, -89);
@@ -148,27 +160,20 @@ public abstract class GeoGridTilerTestCase extends ESTestCase {
     }
 
     public void testGeoGridSetValuesBoundingBoxes_UnboundedGeoShapeCellValues() throws Exception {
-        GeoShapeIndexer indexer = new GeoShapeIndexer(Orientation.CCW, "test");
-        for (int i = 0; i < 1000; i++) {
+        for (int i = 0; i < 100; i++) {
             int precision = randomIntBetween(0, 3);
-            Geometry geometry = randomValueOtherThanMany(g -> {
-                try {
-                    indexer.indexShape(g);
-                    return false;
-                } catch (Exception e) {
-                    return true;
-                }
-            }, () -> boxToGeo(randomBBox()));
+            Geometry geometry = randomValueOtherThanMany(this::geometryIsInvalid, () -> boxToGeo(randomBBox()));
             GeoShapeValues.GeoShapeValue value = geoShapeValue(geometry);
             GeoShapeCellValues unboundedCellValues = new GeoShapeCellValues(
                 makeGeoShapeValues(value),
                 getUnboundedGridTiler(precision),
                 NOOP_BREAKER
             );
+
             assertTrue(unboundedCellValues.advanceExact(0));
-            int numTiles = unboundedCellValues.docValueCount();
+            int numBuckets = unboundedCellValues.docValueCount();
             int expected = expectedBuckets(value, precision, null);
-            assertThat(numTiles, equalTo(expected));
+            assertThat("[" + i + ":" + precision + "] bucket count", numBuckets, equalTo(expected));
         }
     }
 
@@ -191,6 +196,7 @@ public abstract class GeoGridTilerTestCase extends ESTestCase {
 
     public void testBoundsExcludeTouchingTiles() throws Exception {
         final int precision = randomIntBetween(4, maxPrecision()) - 4;
+        assumeTrue("Test only works for rectangular tilers", isRectangularTiler());
 
         final Rectangle rectangle = getCell(GeoTestUtil.nextLongitude(), GeoTestUtil.nextLatitude(), precision);
         final GeoBoundingBox box = new GeoBoundingBox(
@@ -210,7 +216,7 @@ public abstract class GeoGridTilerTestCase extends ESTestCase {
             assertTrue(values.advanceExact(0));
             final int numTiles = values.docValueCount();
             final int expected = (int) getCellsForDiffPrecision(i);
-            assertThat(numTiles, equalTo(expected));
+            assertThat("For precision " + (precision + i), numTiles, equalTo(expected));
         }
     }
 
@@ -229,7 +235,7 @@ public abstract class GeoGridTilerTestCase extends ESTestCase {
         final long maxNumBytes;
         final long curNumBytes;
         if (byteChangeHistory.size() == 1) {
-            curNumBytes = maxNumBytes = byteChangeHistory.get(byteChangeHistory.size() - 1);
+            curNumBytes = maxNumBytes = byteChangeHistory.get(0);
         } else {
             long oldNumBytes = -byteChangeHistory.get(byteChangeHistory.size() - 1);
             curNumBytes = byteChangeHistory.get(byteChangeHistory.size() - 2);
@@ -252,6 +258,16 @@ public abstract class GeoGridTilerTestCase extends ESTestCase {
         });
     }
 
+    protected boolean geometryIsInvalid(Geometry g) {
+        try {
+            // make sure is a valid shape
+            new GeoShapeIndexer(Orientation.CCW, "test").indexShape(g);
+            return false;
+        } catch (Exception e) {
+            return true;
+        }
+    }
+
     protected GeoShapeValues makeGeoShapeValues(GeoShapeValues.GeoShapeValue... values) {
         return new GeoShapeValues() {
             int index = 0;

+ 214 - 0
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoHexTilerTests.java

@@ -0,0 +1,214 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid;
+
+import org.apache.lucene.geo.GeoEncodingUtils;
+import org.apache.lucene.util.ArrayUtil;
+import org.elasticsearch.common.geo.GeoBoundingBox;
+import org.elasticsearch.common.geo.GeoPoint;
+import org.elasticsearch.geo.GeometryTestUtils;
+import org.elasticsearch.geometry.Geometry;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.Rectangle;
+import org.elasticsearch.geometry.utils.WellKnownText;
+import org.elasticsearch.h3.H3;
+import org.elasticsearch.xpack.spatial.common.H3CartesianUtil;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoRelation;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoShapeValues;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import static org.elasticsearch.xpack.spatial.util.GeoTestUtils.geoShapeValue;
+import static org.hamcrest.Matchers.equalTo;
+
+public class GeoHexTilerTests extends GeoGridTilerTestCase {
+    @Override
+    protected GeoGridTiler getUnboundedGridTiler(int precision) {
+        return new UnboundedGeoHexGridTiler(precision);
+    }
+
+    @Override
+    protected GeoGridTiler getBoundedGridTiler(GeoBoundingBox bbox, int precision) {
+        return new BoundedGeoHexGridTiler(precision, bbox);
+    }
+
+    @Override
+    protected int maxPrecision() {
+        return H3.MAX_H3_RES;
+    }
+
+    @Override
+    protected Rectangle getCell(double lon, double lat, int precision) {
+        return H3CartesianUtil.toBoundingBox(H3.geoToH3(lat, lon, precision));
+    }
+
+    /** The H3 tilers does not produce rectangular tiles, and some tests assume this */
+    @Override
+    protected boolean isRectangularTiler() {
+        return false;
+    }
+
+    @Override
+    protected long getCellsForDiffPrecision(int precisionDiff) {
+        return UnboundedGeoHexGridTiler.calcMaxAddresses(precisionDiff);
+    }
+
+    public void testLargeShape() throws Exception {
+        // We have a shape and a tile both covering all mercator space, so we expect all level0 H3 cells to match
+        Rectangle shapeRectangle = new Rectangle(-180, 180, 90, -90);
+        GeoShapeValues.GeoShapeValue value = geoShapeValue(shapeRectangle);
+
+        GeoBoundingBox boundingBox = new GeoBoundingBox(
+            new GeoPoint(shapeRectangle.getMaxLat(), shapeRectangle.getMinLon()),
+            new GeoPoint(shapeRectangle.getMinLat(), shapeRectangle.getMaxLon())
+        );
+
+        for (int precision = 0; precision < 4; precision++) {
+            GeoShapeCellValues values = new GeoShapeCellValues(
+                makeGeoShapeValues(value),
+                getBoundedGridTiler(boundingBox, precision),
+                NOOP_BREAKER
+            );
+            assertTrue(values.advanceExact(0));
+            int numTiles = values.docValueCount();
+            int expectedTiles = expectedBuckets(value, precision, boundingBox);
+            assertThat(expectedTiles, equalTo(numTiles));
+        }
+    }
+
+    public void testLargeShapeWithBounds() throws Exception {
+        // We have a shape covering all space
+        Rectangle shapeRectangle = new Rectangle(-180, 180, 90, -90);
+        GeoShapeValues.GeoShapeValue value = geoShapeValue(shapeRectangle);
+
+        Point point = GeometryTestUtils.randomPoint();
+        int res = randomIntBetween(0, H3.MAX_H3_RES - 4);
+        long h3 = H3.geoToH3(point.getLat(), point.getLon(), res);
+        Rectangle tile = H3CartesianUtil.toBoundingBox(h3);
+        GeoBoundingBox boundingBox = new GeoBoundingBox(
+            new GeoPoint(
+                GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(tile.getMaxLat())),
+                GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(tile.getMinLon()))
+            ),
+            new GeoPoint(
+                GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(tile.getMinLat())),
+                GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(tile.getMaxLon()))
+            )
+        );
+
+        for (int precision = res; precision < res + 4; precision++) {
+            String msg = "Failed " + WellKnownText.toWKT(point) + " at resolution " + res + " with precision " + precision;
+            GeoShapeCellValues values = new GeoShapeCellValues(
+                makeGeoShapeValues(value),
+                getBoundedGridTiler(boundingBox, precision),
+                NOOP_BREAKER
+            );
+            assertTrue(values.advanceExact(0));
+            long[] h3bins = ArrayUtil.copyOfSubArray(values.getValues(), 0, values.docValueCount());
+            assertCorner(h3bins, new Point(tile.getMinLon(), tile.getMinLat()), precision, msg);
+            assertCorner(h3bins, new Point(tile.getMaxLon(), tile.getMinLat()), precision, msg);
+            assertCorner(h3bins, new Point(tile.getMinLon(), tile.getMaxLat()), precision, msg);
+            assertCorner(h3bins, new Point(tile.getMaxLon(), tile.getMaxLat()), precision, msg);
+        }
+    }
+
+    private void assertCorner(long[] h3bins, Point point, int precision, String msg) throws IOException {
+        GeoShapeValues.GeoShapeValue cornerValue = geoShapeValue(point);
+        GeoShapeCellValues cornerValues = new GeoShapeCellValues(
+            makeGeoShapeValues(cornerValue),
+            getUnboundedGridTiler(precision),
+            NOOP_BREAKER
+        );
+        assertTrue(cornerValues.advanceExact(0));
+        long[] h3binsCorner = ArrayUtil.copyOfSubArray(cornerValues.getValues(), 0, cornerValues.docValueCount());
+        for (long corner : h3binsCorner) {
+            assertTrue(msg, Arrays.binarySearch(h3bins, corner) != -1);
+        }
+    }
+
+    @Override
+    protected void assertSetValuesBruteAndRecursive(Geometry geometry) throws Exception {
+        int precision = randomIntBetween(1, 4);
+        UnboundedGeoHexGridTiler tiler = new UnboundedGeoHexGridTiler(precision);
+        GeoShapeValues.GeoShapeValue value = geoShapeValue(geometry);
+
+        GeoShapeCellValues recursiveValues = new GeoShapeCellValues(null, tiler, NOOP_BREAKER);
+        int recursiveCount = tiler.setValuesByRecursion(recursiveValues, value, value.boundingBox());
+
+        GeoShapeCellValues bruteForceValues = new GeoShapeCellValues(null, tiler, NOOP_BREAKER);
+        int bruteForceCount = 0;
+        for (long h3 : H3.getLongRes0Cells()) {
+            bruteForceCount = addBruteForce(tiler, bruteForceValues, value, h3, precision, bruteForceCount);
+        }
+
+        long[] recursive = Arrays.copyOf(recursiveValues.getValues(), recursiveCount);
+        long[] bruteForce = Arrays.copyOf(bruteForceValues.getValues(), bruteForceCount);
+
+        Arrays.sort(recursive);
+        Arrays.sort(bruteForce);
+        assertArrayEquals(geometry.toString(), recursive, bruteForce);
+    }
+
+    private int addBruteForce(
+        AbstractGeoHexGridTiler tiler,
+        GeoShapeCellValues values,
+        GeoShapeValues.GeoShapeValue geoValue,
+        long h3,
+        int precision,
+        int valueIndex
+    ) throws IOException {
+        if (H3.getResolution(h3) == precision) {
+            if (tiler.relateTile(geoValue, h3) != GeoRelation.QUERY_DISJOINT) {
+                values.resizeCell(valueIndex + 1);
+                values.add(valueIndex++, h3);
+            }
+        } else {
+            for (long child : H3.h3ToChildren(h3)) {
+                valueIndex = addBruteForce(tiler, values, geoValue, child, precision, valueIndex);
+            }
+        }
+        return valueIndex;
+    }
+
+    @Override
+    protected int expectedBuckets(GeoShapeValues.GeoShapeValue geoValue, int precision, GeoBoundingBox bbox) throws Exception {
+        return computeBuckets(H3.getLongRes0Cells(), bbox, geoValue, precision);
+    }
+
+    private int computeBuckets(long[] children, GeoBoundingBox bbox, GeoShapeValues.GeoShapeValue geoValue, int finalPrecision)
+        throws IOException {
+        int count = 0;
+        for (long child : children) {
+            if (H3.getResolution(child) == finalPrecision) {
+                if (intersects(child, geoValue, bbox, finalPrecision)) {
+                    count++;
+                }
+            } else {
+                count += computeBuckets(H3.h3ToChildren(child), bbox, geoValue, finalPrecision);
+            }
+        }
+        return count;
+    }
+
+    private boolean intersects(long h3, GeoShapeValues.GeoShapeValue geoValue, GeoBoundingBox bbox, int finalPrecision) throws IOException {
+        if (addressIntersectsBounds(h3, bbox, finalPrecision) == false) {
+            return false;
+        }
+        UnboundedGeoHexGridTiler predicate = new UnboundedGeoHexGridTiler(finalPrecision);
+        return predicate.relateTile(geoValue, h3) != GeoRelation.QUERY_DISJOINT;
+    }
+
+    private boolean addressIntersectsBounds(long h3, GeoBoundingBox bbox, int finalPrecision) {
+        if (bbox == null) {
+            return true;
+        }
+        BoundedGeoHexGridTiler predicate = new BoundedGeoHexGridTiler(finalPrecision, bbox);
+        return predicate.h3IntersectsBounds(h3);
+    }
+}

+ 198 - 0
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoHexVisitorTests.java

@@ -0,0 +1,198 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid;
+
+import org.apache.lucene.tests.geo.GeoTestUtil;
+import org.elasticsearch.common.geo.GeometryNormalizer;
+import org.elasticsearch.common.geo.Orientation;
+import org.elasticsearch.geometry.Geometry;
+import org.elasticsearch.geometry.GeometryCollection;
+import org.elasticsearch.geometry.Line;
+import org.elasticsearch.geometry.LinearRing;
+import org.elasticsearch.geometry.MultiPoint;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.Polygon;
+import org.elasticsearch.geometry.Rectangle;
+import org.elasticsearch.h3.H3;
+import org.elasticsearch.h3.LatLng;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xpack.spatial.index.fielddata.CoordinateEncoder;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoRelation;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeometryDocValueReader;
+import org.elasticsearch.xpack.spatial.util.GeoTestUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.LongFunction;
+
+public class GeoHexVisitorTests extends ESTestCase {
+
+    public void testPoint() throws IOException {
+        doTestGeometry(GeoHexVisitorTests::getGeometryAsPoints, false);
+    }
+
+    public void testLine() throws IOException {
+        doTestGeometry(GeoHexVisitorTests::getGeometryAsLine, false);
+    }
+
+    public void testTriangle() throws IOException {
+        doTestGeometry(GeoHexVisitorTests::getGeometryAsPolygon, true);
+    }
+
+    private void doTestGeometry(LongFunction<Geometry> h3ToGeometry, boolean hasArea) throws IOException {
+        // we ignore polar cells are they are problematic and do not keep the relationships
+        long h3 = randomValueOtherThanMany(
+            l -> l == H3.geoToH3(90, 0, H3.getResolution(l)) || l == H3.geoToH3(-90, 0, H3.getResolution(l)),
+            () -> H3.geoToH3(GeoTestUtil.nextLatitude(), GeoTestUtil.nextLongitude(), randomIntBetween(2, 14))
+        );
+        long centerChild = H3.childPosToH3(h3, 0);
+        // children position 3 is chosen so we never use a polar polygon
+        long noChildIntersecting = H3.noChildIntersectingPosToH3(h3, 3);
+        GeoHexVisitor visitor = new GeoHexVisitor();
+        visitor.reset(h3);
+        final String failMsg = "failing h3: " + h3;
+        boolean h3CrossesDateline = visitor.getLeftX() > visitor.getRightX();
+        {
+            GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(h3ToGeometry.apply(h3), CoordinateEncoder.GEO);
+            visitor.reset(h3);
+            reader.visit(visitor);
+            assertEquals(failMsg, GeoRelation.QUERY_CROSSES, visitor.relation());
+
+            Rectangle rectangle = getGeometryAsRectangle(h3);
+            assertTrue(failMsg, visitor.intersectsBbox(rectangle.getMinX(), rectangle.getMaxX(), rectangle.getMinY(), rectangle.getMaxY()));
+        }
+        {
+            GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(h3ToGeometry.apply(centerChild), CoordinateEncoder.GEO);
+            visitor.reset(h3);
+            reader.visit(visitor);
+            assertEquals("failing h3: " + h3, GeoRelation.QUERY_CONTAINS, visitor.relation());
+
+            Rectangle rectangle = getGeometryAsRectangle(centerChild);
+            assertTrue(failMsg, visitor.intersectsBbox(rectangle.getMinX(), rectangle.getMaxX(), rectangle.getMinY(), rectangle.getMaxY()));
+        }
+        {
+            GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(h3ToGeometry.apply(h3), CoordinateEncoder.GEO);
+            visitor.reset(centerChild);
+            reader.visit(visitor);
+            if (hasArea) {
+                if (h3CrossesDateline && visitor.getLeftX() > visitor.getRightX()) {
+                    // if both polygons crosses the dateline it cannot be inside due to the polygon splitting technique
+                    assertEquals("failing h3: " + h3, GeoRelation.QUERY_CROSSES, visitor.relation());
+                } else {
+                    assertEquals("failing h3: " + h3, GeoRelation.QUERY_INSIDE, visitor.relation());
+                }
+            } else {
+                assertEquals("failing h3: " + h3, GeoRelation.QUERY_DISJOINT, visitor.relation());
+            }
+        }
+        {
+            GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(
+                h3ToGeometry.apply(noChildIntersecting),
+                CoordinateEncoder.GEO
+            );
+            visitor.reset(centerChild);
+            reader.visit(visitor);
+            assertEquals("failing h3: " + h3, GeoRelation.QUERY_DISJOINT, visitor.relation());
+
+            Rectangle rectangle = getGeometryAsRectangle(noChildIntersecting);
+            assertFalse(
+                failMsg,
+                visitor.intersectsBbox(rectangle.getMinX(), rectangle.getMaxX(), rectangle.getMinY(), rectangle.getMaxY())
+            );
+        }
+        {
+            GeometryCollection<Geometry> collection = new GeometryCollection<>(
+                List.of(h3ToGeometry.apply(centerChild), h3ToGeometry.apply(noChildIntersecting))
+            );
+            GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(collection, CoordinateEncoder.GEO);
+            visitor.reset(h3);
+            reader.visit(visitor);
+            assertEquals("failing h3: " + h3, GeoRelation.QUERY_CROSSES, visitor.relation());
+        }
+        {
+            LatLng latLng1 = H3.h3ToLatLng(centerChild);
+            LatLng latLng2 = H3.h3ToLatLng(noChildIntersecting);
+            MultiPoint multiPoint = new MultiPoint(
+                List.of(new Point(latLng1.getLonDeg(), latLng1.getLatDeg()), new Point(latLng2.getLonDeg(), latLng2.getLatDeg()))
+            );
+            GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(multiPoint, CoordinateEncoder.GEO);
+            visitor.reset(h3);
+            reader.visit(visitor);
+            assertEquals("failing h3: " + h3, GeoRelation.QUERY_CROSSES, visitor.relation());
+        }
+    }
+
+    private static Geometry getGeometryAsPolygon(long h3) {
+        final GeoHexVisitor visitor = new GeoHexVisitor();
+        visitor.reset(h3);
+        final Polygon polygon = new Polygon(new LinearRing(visitor.getXs(), visitor.getYs()));
+        if (visitor.getLeftX() > visitor.getRightX()) {
+            final Geometry geometry = GeometryNormalizer.apply(Orientation.CCW, polygon);
+            if (geometry instanceof Polygon) {
+                // there is a bug on the code that breaks polygons across the dateline
+                // when polygon is close to the pole (I think) so we need to try again
+                return GeometryNormalizer.apply(Orientation.CW, polygon);
+            }
+            return geometry;
+        } else {
+            return polygon;
+        }
+    }
+
+    private static Geometry getGeometryAsLine(long h3) {
+        final GeoHexVisitor visitor = new GeoHexVisitor();
+        visitor.reset(h3);
+        if (visitor.getLeftX() > visitor.getRightX()) {
+            double[] translatedXs = visitor.getXs();
+            for (int i = 0; i < translatedXs.length; i++) {
+                translatedXs[i] = translatedXs[i] < 0 ? translatedXs[i] + 360 : translatedXs[i];
+            }
+            final Geometry geometry = GeometryNormalizer.apply(Orientation.CCW, new Line(translatedXs, visitor.getYs()));
+            return GeometryNormalizer.apply(Orientation.CW, geometry);
+        } else {
+            return new Line(visitor.getXs(), visitor.getYs());
+        }
+    }
+
+    private static Geometry getGeometryAsPoints(long h3) {
+        final GeoHexVisitor visitor = new GeoHexVisitor();
+        visitor.reset(h3);
+        List<Point> points = new ArrayList<>();
+        double[] xs = visitor.getXs();
+        double[] ys = visitor.getYs();
+        for (int i = 0; i < xs.length; i++) {
+            points.add(new Point(xs[i], ys[i]));
+        }
+        return new MultiPoint(points);
+    }
+
+    private static Rectangle getGeometryAsRectangle(long h3) {
+        final GeoHexVisitor visitor = new GeoHexVisitor();
+        visitor.reset(h3);
+        return new Rectangle(visitor.getLeftX(), visitor.getRightX(), visitor.getMaxY(), visitor.getMinY());
+    }
+
+    public void testLongGeometriesWithDateline() throws IOException {
+        long h3 = H3.geoToH3(0, 180, randomIntBetween(0, 4));
+        GeoHexVisitor visitor = new GeoHexVisitor();
+        visitor.reset(h3);
+        {
+            Line line = new Line(new double[] { -180, 180 }, new double[] { 0, 0 });
+            GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(line, CoordinateEncoder.GEO);
+            reader.visit(visitor);
+            assertEquals(GeoRelation.QUERY_CROSSES, visitor.relation());
+        }
+        {
+            Polygon polygon = new Polygon(new LinearRing(new double[] { -180, 180, 180, -180 }, new double[] { -1, -1, 1, -1 }));
+            GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(polygon, CoordinateEncoder.GEO);
+            reader.visit(visitor);
+            assertEquals(GeoRelation.QUERY_CROSSES, visitor.relation());
+        }
+    }
+}

+ 4 - 6
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoShapeGeoGridTestCase.java

@@ -20,7 +20,6 @@ import org.elasticsearch.core.CheckedConsumer;
 import org.elasticsearch.geometry.Geometry;
 import org.elasticsearch.geometry.MultiPoint;
 import org.elasticsearch.geometry.Point;
-import org.elasticsearch.geometry.Rectangle;
 import org.elasticsearch.index.mapper.MappedFieldType;
 import org.elasticsearch.plugins.SearchPlugin;
 import org.elasticsearch.search.aggregations.AggregationBuilder;
@@ -54,7 +53,7 @@ import static org.elasticsearch.xpack.spatial.util.GeoTestUtils.geoShapeValue;
 import static org.hamcrest.Matchers.equalTo;
 
 public abstract class GeoShapeGeoGridTestCase<T extends InternalGeoGridBucket> extends AggregatorTestCase {
-    private static final String FIELD_NAME = "location";
+    protected static final String FIELD_NAME = "location";
 
     /**
      * Generate a random precision according to the rules of the given aggregation.
@@ -77,12 +76,12 @@ public abstract class GeoShapeGeoGridTestCase<T extends InternalGeoGridBucket> e
     protected abstract GeoBoundingBox randomBBox();
 
     /**
-     * Return the bounding tile as a {@link Rectangle} for a given point
+     * Return true if the point intersects the given shape value
      */
     protected abstract boolean intersects(double lng, double lat, int precision, GeoShapeValues.GeoShapeValue value) throws IOException;
 
     /**
-     * Return true if the points intersects the bounds
+     * Return true if the point intersects the given bounding box
      */
     protected abstract boolean intersectsBounds(double lng, double lat, int precision, GeoBoundingBox box);
 
@@ -172,7 +171,6 @@ public abstract class GeoShapeGeoGridTestCase<T extends InternalGeoGridBucket> e
         }
 
         final long numDocsInBucket = numDocsWithin;
-
         testCase(new MatchAllDocsQuery(), FIELD_NAME, precision, bbox, iw -> {
             for (BinaryShapeDocValuesField docField : docs) {
                 iw.addDocument(Collections.singletonList(docField));
@@ -248,7 +246,7 @@ public abstract class GeoShapeGeoGridTestCase<T extends InternalGeoGridBucket> e
     }
 
     @SuppressWarnings("unchecked")
-    private void testCase(
+    protected void testCase(
         Query query,
         int precision,
         GeoBoundingBox geoBoundingBox,

+ 88 - 0
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoShapeGeoHexGridAggregatorTests.java

@@ -0,0 +1,88 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid;
+
+import org.apache.lucene.document.SortedSetDocValuesField;
+import org.apache.lucene.geo.LatLonGeometry;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.geo.GeoBoundingBox;
+import org.elasticsearch.geo.GeometryTestUtils;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.h3.H3;
+import org.elasticsearch.search.aggregations.bucket.geogrid.GeoGridAggregationBuilder;
+import org.elasticsearch.xpack.spatial.common.H3CartesianUtil;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoRelation;
+import org.elasticsearch.xpack.spatial.index.fielddata.GeoShapeValues;
+import org.elasticsearch.xpack.spatial.util.GeoTestUtils;
+
+import java.io.IOException;
+import java.util.Collections;
+
+public class GeoShapeGeoHexGridAggregatorTests extends GeoShapeGeoGridTestCase<InternalGeoHexGridBucket> {
+    @Override
+    protected int randomPrecision() {
+        return randomIntBetween(0, H3.MAX_H3_RES);
+    }
+
+    @Override
+    protected String hashAsString(double lng, double lat, int precision) {
+        // TODO: In theory we can have more than one hash per point?
+        final long h3 = H3.geoToH3(lat, lng, precision);
+        if (LatLonGeometry.create(H3CartesianUtil.getLatLonGeometry(h3)).contains(lng, lat)) {
+            return H3.h3ToString(h3);
+        }
+        for (long n : H3.hexRing(h3)) {
+            if (LatLonGeometry.create(H3CartesianUtil.getLatLonGeometry(n)).contains(lng, lat)) {
+                return H3.h3ToString(n);
+            }
+        }
+        fail("Could not find valid h3 bin");
+        return null;
+    }
+
+    @Override
+    protected Point randomPoint() {
+        return GeometryTestUtils.randomPoint();
+    }
+
+    @Override
+    protected GeoBoundingBox randomBBox() {
+        return GeoTestUtils.randomBBox();
+    }
+
+    @Override
+    protected boolean intersects(double lng, double lat, int precision, GeoShapeValues.GeoShapeValue value) throws IOException {
+        return value.relate(new org.apache.lucene.geo.Point(lat, lng)) != GeoRelation.QUERY_DISJOINT;
+    }
+
+    @Override
+    protected boolean intersectsBounds(double lng, double lat, int precision, GeoBoundingBox box) {
+        final BoundedGeoHexGridTiler tiler = new BoundedGeoHexGridTiler(precision, box);
+        return tiler.h3IntersectsBounds(H3.stringToH3(hashAsString(lng, lat, precision)));
+    }
+
+    @Override
+    protected GeoGridAggregationBuilder createBuilder(String name) {
+        return new GeoHexGridAggregationBuilder(name);
+    }
+
+    @Override
+    public void testMappedMissingGeoShape() throws IOException {
+        final String lineString = "LINESTRING (30 10, 10 30, 40 40)";
+        final GeoGridAggregationBuilder builder = createBuilder("_name").field(FIELD_NAME).missing(lineString);
+        testCase(
+            new MatchAllDocsQuery(),
+            1,
+            null,
+            iw -> { iw.addDocument(Collections.singleton(new SortedSetDocValuesField("string", new BytesRef("a")))); },
+            geoGrid -> { assertEquals(8, geoGrid.getBuckets().size()); },
+            builder
+        );
+    }
+}

+ 5 - 16
x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/GridAggregation.java

@@ -19,6 +19,7 @@ import org.elasticsearch.h3.LatLng;
 import org.elasticsearch.search.aggregations.bucket.geogrid.GeoGridAggregationBuilder;
 import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileGridAggregationBuilder;
 import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils;
+import org.elasticsearch.xpack.spatial.common.H3CartesianUtil;
 import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.GeoHexGridAggregationBuilder;
 import org.elasticsearch.xpack.vectortile.feature.FeatureFactory;
 
@@ -118,6 +119,7 @@ enum GridAggregation {
             2,
             3,
             3,
+            3,
             4,
             4,
             5,
@@ -125,16 +127,15 @@ enum GridAggregation {
             6,
             7,
             8,
-            8,
+            9,
             9,
             10,
             11,
             11,
             12,
             13,
-            13,
             14,
-            15,
+            14,
             15,
             15,
             15,
@@ -188,19 +189,7 @@ enum GridAggregation {
 
         @Override
         public Rectangle toRectangle(String bucketKey) {
-            final CellBoundary boundary = H3.h3ToGeoBoundary(bucketKey);
-            double minLat = Double.POSITIVE_INFINITY;
-            double minLon = Double.POSITIVE_INFINITY;
-            double maxLat = Double.NEGATIVE_INFINITY;
-            double maxLon = Double.NEGATIVE_INFINITY;
-            for (int i = 0; i < boundary.numPoints(); i++) {
-                final LatLng latLng = boundary.getLatLon(i);
-                minLat = Math.min(minLat, latLng.getLatDeg());
-                minLon = Math.min(minLon, latLng.getLonDeg());
-                maxLat = Math.max(maxLat, latLng.getLatDeg());
-                maxLon = Math.max(maxLon, latLng.getLonDeg());
-            }
-            return new Rectangle(minLon, maxLon, maxLat, minLat);
+            return H3CartesianUtil.toBoundingBox(H3.stringToH3(bucketKey));
         }
     };