You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by iv...@apache.org on 2021/01/08 08:21:13 UTC

[lucene-solr] branch branch_8x updated: LUCENE-9641: Support for spatial relationships in LatLonPoint (#2155) (#2188)

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

ivera pushed a commit to branch branch_8x
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git


The following commit(s) were added to refs/heads/branch_8x by this push:
     new f73f6b1  LUCENE-9641: Support for spatial relationships in LatLonPoint (#2155) (#2188)
f73f6b1 is described below

commit f73f6b11f0ed4bcbfce445152d813adb3327d249
Author: Ignacio Vera <iv...@apache.org>
AuthorDate: Fri Jan 8 09:20:51 2021 +0100

    LUCENE-9641: Support for spatial relationships in LatLonPoint (#2155) (#2188)
    
    Equivalent to LatLonShape, LatLonPoint can be queried now using spatial relationships.
---
 lucene/CHANGES.txt                                 |   2 +
 .../lucene/document/LatLonDocValuesField.java      | 192 +++---
 .../LatLonDocValuesPointInGeometryQuery.java       | 166 -----
 .../lucene/document/LatLonDocValuesQuery.java      | 272 ++++++++
 .../org/apache/lucene/document/LatLonPoint.java    | 212 +++---
 .../document/LatLonPointInGeometryQuery.java       | 275 --------
 .../apache/lucene/document/LatLonPointQuery.java   | 183 ++++++
 .../document/LatLonShapeBoundingBoxQuery.java      | 536 ++++++++-------
 .../apache/lucene/document/LatLonShapeQuery.java   | 258 +++++---
 .../org/apache/lucene/document/ShapeQuery.java     | 621 ------------------
 .../org/apache/lucene/document/SpatialQuery.java   | 722 +++++++++++++++++++++
 .../org/apache/lucene/document/XYShapeQuery.java   | 333 +++++-----
 .../org/apache/lucene/geo/GeoEncodingUtils.java    | 212 +++---
 .../src/java/org/apache/lucene/geo/Point2D.java    | 109 +++-
 .../document/BaseLatLonDocValueTestCase.java       |  72 ++
 .../lucene/document/BaseLatLonPointTestCase.java   | 139 ++++
 .../lucene/document/BaseLatLonShapeTestCase.java   | 361 ++++-------
 .../lucene/document/BaseLatLonSpatialTestCase.java | 220 +++++++
 ...ShapeTestCase.java => BaseSpatialTestCase.java} | 199 ++++--
 .../lucene/document/BaseXYShapeTestCase.java       |  49 +-
 .../TestLatLonDocValuesMultiPointPointQueries.java |  99 +++
 .../TestLatLonDocValuesPointPointQueries.java      |  71 ++
 .../document/TestLatLonMultiPointPointQueries.java |  99 +++
 .../document/TestLatLonPointPointQueries.java      |  69 ++
 .../apache/lucene/document/TestLatLonShape.java    |   9 +
 .../lucene/search/TestLatLonDocValuesQueries.java  |   4 +-
 .../lucene/search/TestLatLonPointQueries.java      |   3 +-
 27 files changed, 3362 insertions(+), 2125 deletions(-)

diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 563d8e2..bfc1add 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -14,6 +14,8 @@ New Features
 
 * LUCENE-9552: New LatLonPoint query that accepts an array of LatLonGeometries. (Ignacio Vera)  
 
+* LUCENE-9641: LatLonPoint query support for spatial relationships. (Ignacio Vera) 
+
 * LUCENE-9553: New XYPoint query that accepts an array of XYGeometries. (Ignacio Vera)
 
 * LUCENE-9594: FeatureField supports newLinearQuery that for scoring uses raw indexed
diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java
index e263f78..b1e7329 100644
--- a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java
+++ b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java
@@ -23,6 +23,7 @@ import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
 
 import org.apache.lucene.geo.Circle;
 import org.apache.lucene.geo.LatLonGeometry;
+import org.apache.lucene.geo.Point;
 import org.apache.lucene.geo.Polygon;
 import org.apache.lucene.geo.Rectangle;
 import org.apache.lucene.index.DocValuesType;
@@ -33,55 +34,63 @@ import org.apache.lucene.search.MatchNoDocsQuery;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.SortField;
 
-/** 
+/**
  * An per-document location field.
- * <p>
- * Sorting by distance is efficient. Multiple values for the same field in one document
- * is allowed. 
- * <p>
- * This field defines static factory methods for common operations:
+ *
+ * <p>Sorting by distance is efficient. Multiple values for the same field in one document is
+ * allowed.
+ *
+ * <p>This field defines static factory methods for common operations:
+ *
  * <ul>
- *   <li>{@link #newDistanceSort newDistanceSort()} for ordering documents by distance from a specified location. 
+ *   <li>{@link #newDistanceSort newDistanceSort()} for ordering documents by distance from a
+ *       specified location.
  * </ul>
- * <p>
- * If you also need query operations, you should add a separate {@link LatLonPoint} instance.
- * If you also need to store the value, you should add a separate {@link StoredField} instance.
- * <p>
- * <b>WARNING</b>: Values are indexed with some loss of precision from the
- * original {@code double} values (4.190951585769653E-8 for the latitude component
- * and 8.381903171539307E-8 for longitude).
+ *
+ * <p>If you also need query operations, you should add a separate {@link LatLonPoint} instance. If
+ * you also need to store the value, you should add a separate {@link StoredField} instance.
+ *
+ * <p><b>WARNING</b>: Values are indexed with some loss of precision from the original {@code
+ * double} values (4.190951585769653E-8 for the latitude component and 8.381903171539307E-8 for
+ * longitude).
+ *
  * @see LatLonPoint
  */
 public class LatLonDocValuesField extends Field {
 
   /**
    * Type for a LatLonDocValuesField
-   * <p>
-   * Each value stores a 64-bit long where the upper 32 bits are the encoded latitude,
-   * and the lower 32 bits are the encoded longitude.
+   *
+   * <p>Each value stores a 64-bit long where the upper 32 bits are the encoded latitude, and the
+   * lower 32 bits are the encoded longitude.
+   *
    * @see org.apache.lucene.geo.GeoEncodingUtils#decodeLatitude(int)
    * @see org.apache.lucene.geo.GeoEncodingUtils#decodeLongitude(int)
    */
   public static final FieldType TYPE = new FieldType();
+
   static {
     TYPE.setDocValuesType(DocValuesType.SORTED_NUMERIC);
     TYPE.freeze();
   }
-  
-  /** 
+
+  /**
    * Creates a new LatLonDocValuesField with the specified latitude and longitude
+   *
    * @param name field name
    * @param latitude latitude value: must be within standard +/-90 coordinate bounds.
    * @param longitude longitude value: must be within standard +/-180 coordinate bounds.
-   * @throws IllegalArgumentException if the field name is null or latitude or longitude are out of bounds
+   * @throws IllegalArgumentException if the field name is null or latitude or longitude are out of
+   *     bounds
    */
   public LatLonDocValuesField(String name, double latitude, double longitude) {
     super(name, TYPE);
     setLocationValue(latitude, longitude);
   }
-  
+
   /**
    * Change the values of this field
+   *
    * @param latitude latitude value: must be within standard +/-90 coordinate bounds.
    * @param longitude longitude value: must be within standard +/-180 coordinate bounds.
    * @throws IllegalArgumentException if latitude or longitude are out of bounds
@@ -89,19 +98,28 @@ public class LatLonDocValuesField extends Field {
   public void setLocationValue(double latitude, double longitude) {
     int latitudeEncoded = encodeLatitude(latitude);
     int longitudeEncoded = encodeLongitude(longitude);
-    fieldsData = Long.valueOf((((long)latitudeEncoded) << 32) | (longitudeEncoded & 0xFFFFFFFFL));
+    fieldsData = Long.valueOf((((long) latitudeEncoded) << 32) | (longitudeEncoded & 0xFFFFFFFFL));
   }
 
-  /** helper: checks a fieldinfo and throws exception if its definitely not a LatLonDocValuesField */
+  /**
+   * helper: checks a fieldinfo and throws exception if its definitely not a LatLonDocValuesField
+   */
   static void checkCompatible(FieldInfo fieldInfo) {
-    // dv properties could be "unset", if you e.g. used only StoredField with this same name in the segment.
-    if (fieldInfo.getDocValuesType() != DocValuesType.NONE && fieldInfo.getDocValuesType() != TYPE.docValuesType()) {
-      throw new IllegalArgumentException("field=\"" + fieldInfo.name + "\" was indexed with docValuesType=" + fieldInfo.getDocValuesType() + 
-                                         " but this type has docValuesType=" + TYPE.docValuesType() + 
-                                         ", is the field really a LatLonDocValuesField?");
+    // dv properties could be "unset", if you e.g. used only StoredField with this same name in the
+    // segment.
+    if (fieldInfo.getDocValuesType() != DocValuesType.NONE
+            && fieldInfo.getDocValuesType() != TYPE.docValuesType()) {
+      throw new IllegalArgumentException(
+              "field=\""
+                      + fieldInfo.name
+                      + "\" was indexed with docValuesType="
+                      + fieldInfo.getDocValuesType()
+                      + " but this type has docValuesType="
+                      + TYPE.docValuesType()
+                      + ", is the field really a LatLonDocValuesField?");
     }
   }
-  
+
   @Override
   public String toString() {
     StringBuilder result = new StringBuilder();
@@ -110,10 +128,10 @@ public class LatLonDocValuesField extends Field {
     result.append(name);
     result.append(':');
 
-    long currentValue = (Long)fieldsData;
-    result.append(decodeLatitude((int)(currentValue >> 32)));
+    long currentValue = (Long) fieldsData;
+    result.append(decodeLatitude((int) (currentValue >> 32)));
     result.append(',');
-    result.append(decodeLongitude((int)(currentValue & 0xFFFFFFFF)));
+    result.append(decodeLongitude((int) (currentValue & 0xFFFFFFFF)));
 
     result.append('>');
     return result.toString();
@@ -121,15 +139,16 @@ public class LatLonDocValuesField extends Field {
 
   /**
    * Creates a SortField for sorting by distance from a location.
-   * <p>
-   * This sort orders documents by ascending distance from the location. The value returned in {@link FieldDoc} for
-   * the hits contains a Double instance with the distance in meters.
-   * <p>
-   * If a document is missing the field, then by default it is treated as having {@link Double#POSITIVE_INFINITY} distance
-   * (missing values sort last).
-   * <p>
-   * If a document contains multiple values for the field, the <i>closest</i> distance to the location is used.
-   * 
+   *
+   * <p>This sort orders documents by ascending distance from the location. The value returned in
+   * {@link FieldDoc} for the hits contains a Double instance with the distance in meters.
+   *
+   * <p>If a document is missing the field, then by default it is treated as having {@link
+   * Double#POSITIVE_INFINITY} distance (missing values sort last).
+   *
+   * <p>If a document contains multiple values for the field, the <i>closest</i> distance to the
+   * location is used.
+   *
    * @param field field name. must not be null.
    * @param latitude latitude at the center: must be within standard +/-90 coordinate bounds.
    * @param longitude longitude at the center: must be within standard +/-180 coordinate bounds.
@@ -141,15 +160,21 @@ public class LatLonDocValuesField extends Field {
   }
 
   /**
-   * Create a query for matching a bounding box using doc values.
-   * This query is usually slow as it does not use an index structure and needs
-   * to verify documents one-by-one in order to know whether they match. It is
-   * best used wrapped in an {@link IndexOrDocValuesQuery} alongside a
+   * Create a query for matching a bounding box using doc values. This query is usually slow as it
+   * does not use an index structure and needs to verify documents one-by-one in order to know
+   * whether they match. It is best used wrapped in an {@link IndexOrDocValuesQuery} alongside a
    * {@link LatLonPoint#newBoxQuery}.
    */
-  public static Query newSlowBoxQuery(String field, double minLatitude, double maxLatitude, double minLongitude, double maxLongitude) {
-    // exact double values of lat=90.0D and lon=180.0D must be treated special as they are not represented in the encoding
-    // and should not drag in extra bogus junk! TODO: should encodeCeil just throw ArithmeticException to be less trappy here?
+  public static Query newSlowBoxQuery(
+          String field,
+          double minLatitude,
+          double maxLatitude,
+          double minLongitude,
+          double maxLongitude) {
+    // exact double values of lat=90.0D and lon=180.0D must be treated special as they are not
+    // represented in the encoding
+    // and should not drag in extra bogus junk! TODO: should encodeCeil just throw
+    // ArithmeticException to be less trappy here?
     if (minLatitude == 90.0) {
       // range cannot match as 90.0 can never exist
       return new MatchNoDocsQuery("LatLonDocValuesField.newBoxQuery with minLatitude=90.0");
@@ -157,7 +182,8 @@ public class LatLonDocValuesField extends Field {
     if (minLongitude == 180.0) {
       if (maxLongitude == 180.0) {
         // range cannot match as 180.0 can never exist
-        return new MatchNoDocsQuery("LatLonDocValuesField.newBoxQuery with minLongitude=maxLongitude=180.0");
+        return new MatchNoDocsQuery(
+                "LatLonDocValuesField.newBoxQuery with minLongitude=maxLongitude=180.0");
       } else if (maxLongitude < minLongitude) {
         // encodeCeil() with dateline wrapping!
         minLongitude = -180.0;
@@ -167,55 +193,75 @@ public class LatLonDocValuesField extends Field {
   }
 
   /**
-   * Create a query for matching points within the specified distance of the supplied location.
-   * This query is usually slow as it does not use an index structure and needs
-   * to verify documents one-by-one in order to know whether they match. It is
-   * best used wrapped in an {@link IndexOrDocValuesQuery} alongside a
-   * {@link LatLonPoint#newDistanceQuery}.
+   * Create a query for matching points within the specified distance of the supplied location. This
+   * query is usually slow as it does not use an index structure and needs to verify documents
+   * one-by-one in order to know whether they match. It is best used wrapped in an {@link
+   * IndexOrDocValuesQuery} alongside a {@link LatLonPoint#newDistanceQuery}.
+   *
    * @param field field name. must not be null.
    * @param latitude latitude at the center: must be within standard +/-90 coordinate bounds.
    * @param longitude longitude at the center: must be within standard +/-180 coordinate bounds.
-   * @param radiusMeters maximum distance from the center in meters: must be non-negative and finite.
+   * @param radiusMeters maximum distance from the center in meters: must be non-negative and
+   *     finite.
    * @return query matching points within this distance
-   * @throws IllegalArgumentException if {@code field} is null, location has invalid coordinates, or radius is invalid.
+   * @throws IllegalArgumentException if {@code field} is null, location has invalid coordinates, or
+   *     radius is invalid.
    */
-  public static Query newSlowDistanceQuery(String field, double latitude, double longitude, double radiusMeters) {
+  public static Query newSlowDistanceQuery(
+          String field, double latitude, double longitude, double radiusMeters) {
     Circle circle = new Circle(latitude, longitude, radiusMeters);
-    return newSlowGeometryQuery(field, circle);
+    return newSlowGeometryQuery(field, ShapeField.QueryRelation.INTERSECTS, circle);
   }
 
   /**
-   * Create a query for matching points within the supplied polygons.
-   * This query is usually slow as it does not use an index structure and needs
-   * to verify documents one-by-one in order to know whether they match. It is
-   * best used wrapped in an {@link IndexOrDocValuesQuery} alongside a
+   * Create a query for matching points within the supplied polygons. This query is usually slow as
+   * it does not use an index structure and needs to verify documents one-by-one in order to know
+   * whether they match. It is best used wrapped in an {@link IndexOrDocValuesQuery} alongside a
    * {@link LatLonPoint#newPolygonQuery(String, Polygon...)}.
+   *
    * @param field field name. must not be null.
    * @param polygons array of polygons. must not be null or empty.
    * @return query matching points within the given polygons.
-   * @throws IllegalArgumentException if {@code field} is null or polygons is empty or contain a null polygon.
+   * @throws IllegalArgumentException if {@code field} is null or polygons is empty or contain a
+   *     null polygon.
    */
   public static Query newSlowPolygonQuery(String field, Polygon... polygons) {
-    return newSlowGeometryQuery(field, polygons);
+    return newSlowGeometryQuery(field, ShapeField.QueryRelation.INTERSECTS, polygons);
   }
 
   /**
-   * Create a query for matching points within the supplied geometries. Line geometries are not supported.
-   * This query is usually slow as it does not use an index structure and needs
-   * to verify documents one-by-one in order to know whether they match. It is
-   * best used wrapped in an {@link IndexOrDocValuesQuery} alongside a
-   * {@link LatLonPoint#newGeometryQuery(String, LatLonGeometry...)}.
+   * Create a query for matching one or more geometries against the provided {@link
+   * ShapeField.QueryRelation}. Line geometries are not supported for WITHIN relationship. This
+   * query is usually slow as it does not use an index structure and needs to verify documents
+   * one-by-one in order to know whether they match. It is best used wrapped in an {@link
+   * IndexOrDocValuesQuery} alongside a {@link LatLonPoint#newGeometryQuery(String,
+   * ShapeField.QueryRelation, LatLonGeometry...)}.
+   *
    * @param field field name. must not be null.
+   * @param queryRelation The relation the points needs to satisfy with the provided geometries,
+   *     must not be null.
    * @param latLonGeometries array of LatLonGeometries. must not be null or empty.
    * @return query matching points within the given polygons.
-   * @throws IllegalArgumentException if {@code field} is null, {@code latLonGeometries} is null, empty or contain a null or line geometry.
+   * @throws IllegalArgumentException if {@code field} is null, {@code queryRelation} is null,
+   *     {@code latLonGeometries} is null, empty or contain a null or line geometry.
    */
-  public static Query newSlowGeometryQuery(String field, LatLonGeometry... latLonGeometries) {
-    if (latLonGeometries.length == 1 && latLonGeometries[0] instanceof Rectangle) {
+  public static Query newSlowGeometryQuery(
+          String field, ShapeField.QueryRelation queryRelation, LatLonGeometry... latLonGeometries) {
+    if (queryRelation == ShapeField.QueryRelation.INTERSECTS
+            && latLonGeometries.length == 1
+            && latLonGeometries[0] instanceof Rectangle) {
       LatLonGeometry geometry = latLonGeometries[0];
       Rectangle rect = (Rectangle) geometry;
       return newSlowBoxQuery(field, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
     }
-    return new LatLonDocValuesPointInGeometryQuery(field, latLonGeometries);
+    if (queryRelation == ShapeField.QueryRelation.CONTAINS) {
+      for (LatLonGeometry geometry : latLonGeometries) {
+        if ((geometry instanceof Point) == false) {
+          return new MatchNoDocsQuery(
+                  "Contains LatLonDocValuesField.newSlowGeometryQuery with non-point geometries");
+        }
+      }
+    }
+    return new LatLonDocValuesQuery(field, queryRelation, latLonGeometries);
   }
 }
diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesPointInGeometryQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesPointInGeometryQuery.java
deleted file mode 100644
index 04cb612..0000000
--- a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesPointInGeometryQuery.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.lucene.document;
-
-import org.apache.lucene.geo.Component2D;
-import org.apache.lucene.geo.GeoEncodingUtils;
-import org.apache.lucene.geo.LatLonGeometry;
-import org.apache.lucene.geo.Line;
-import org.apache.lucene.index.DocValues;
-import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.SortedNumericDocValues;
-import org.apache.lucene.search.ConstantScoreScorer;
-import org.apache.lucene.search.ConstantScoreWeight;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.QueryVisitor;
-import org.apache.lucene.search.ScoreMode;
-import org.apache.lucene.search.Scorer;
-import org.apache.lucene.search.TwoPhaseIterator;
-import org.apache.lucene.search.Weight;
-
-import java.io.IOException;
-import java.util.Arrays;
-
-/** Geometry query for {@link LatLonDocValuesField}. */
-public class LatLonDocValuesPointInGeometryQuery extends Query {
-
-  private final String field;
-  private final LatLonGeometry[] geometries;
-
-
-  LatLonDocValuesPointInGeometryQuery(String field, LatLonGeometry... geometries) {
-    if (field == null) {
-      throw new IllegalArgumentException("field must not be null");
-    }
-    if (geometries == null) {
-      throw new IllegalArgumentException("geometries must not be null");
-    }
-    if (geometries.length == 0) {
-      throw new IllegalArgumentException("geometries must not be empty");
-    }
-    for (int i = 0; i < geometries.length; i++) {
-      if (geometries[i] == null) {
-        throw new IllegalArgumentException("geometries[" + i + "] must not be null");
-      }
-      if (geometries[i] instanceof Line) {
-        throw new IllegalArgumentException("LatLonDocValuesPointInGeometryQuery does not support queries with line geometries");
-      }
-    }
-    this.field = field;
-    this.geometries = geometries;
-  }
-
-  @Override
-  public String toString(String field) {
-    StringBuilder sb = new StringBuilder();
-    if (!this.field.equals(field)) {
-      sb.append(this.field);
-      sb.append(':');
-    }
-    sb.append("geometries(").append(Arrays.toString(geometries));
-    return sb.append(")").toString();
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (sameClassAs(obj) == false) {
-      return false;
-    }
-    LatLonDocValuesPointInGeometryQuery other = (LatLonDocValuesPointInGeometryQuery) obj;
-    return field.equals(other.field) &&
-           Arrays.equals(geometries, other.geometries);
-  }
-
-  @Override
-  public int hashCode() {
-    int h = classHash();
-    h = 31 * h + field.hashCode();
-    h = 31 * h + Arrays.hashCode(geometries);
-    return h;
-  }
-
-  @Override
-  public void visit(QueryVisitor visitor) {
-    if (visitor.acceptField(field)) {
-      visitor.visitLeaf(this);
-    }
-  }
-
-  @Override
-  public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
-    final Component2D tree = LatLonGeometry.create(geometries);
-
-    if (tree.getMinY() > tree.getMaxY()) {
-      // encodeLatitudeCeil may cause minY to be > maxY iff
-      // the delta between the longitude < the encoding resolution
-      return new ConstantScoreWeight(this, boost) {
-        @Override
-        public Scorer scorer(LeafReaderContext context) {
-          return null;
-        }
-
-        @Override
-        public boolean isCacheable(LeafReaderContext ctx) {
-          return false;
-        }
-      };
-    }
-
-    final GeoEncodingUtils.Component2DPredicate component2DPredicate = GeoEncodingUtils.createComponentPredicate(tree);
-    
-    return new ConstantScoreWeight(this, boost) {
-      
-      @Override
-      public Scorer scorer(LeafReaderContext context) throws IOException {
-        final SortedNumericDocValues values = context.reader().getSortedNumericDocValues(field);
-        if (values == null) {
-          return null;
-        }
-
-        final TwoPhaseIterator iterator = new TwoPhaseIterator(values) {
-
-          @Override
-          public boolean matches() throws IOException {
-            for (int i = 0, count = values.docValueCount(); i < count; ++i) {
-              final long value = values.nextValue();
-              final int lat = (int) (value >>> 32);
-              final int lon = (int) (value & 0xFFFFFFFF);
-              if (component2DPredicate.test(lat, lon)) {
-                return true;
-              }
-            }
-            return false;
-          }
-
-          @Override
-          public float matchCost() {
-            return 1000f; // TODO: what should it be?
-          }
-        };
-        return new ConstantScoreScorer(this, boost, scoreMode, iterator);
-      }
-
-      @Override
-      public boolean isCacheable(LeafReaderContext ctx) {
-        return DocValues.isCacheable(ctx, field);
-      }
-
-    };
-  }
-}
diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesQuery.java
new file mode 100644
index 0000000..8e8120f
--- /dev/null
+++ b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesQuery.java
@@ -0,0 +1,272 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.lucene.document;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.GeoEncodingUtils;
+import org.apache.lucene.geo.LatLonGeometry;
+import org.apache.lucene.geo.Line;
+import org.apache.lucene.geo.Point;
+import org.apache.lucene.index.DocValues;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.SortedNumericDocValues;
+import org.apache.lucene.search.ConstantScoreScorer;
+import org.apache.lucene.search.ConstantScoreWeight;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
+import org.apache.lucene.search.ScoreMode;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.TwoPhaseIterator;
+import org.apache.lucene.search.Weight;
+
+/**
+ * Finds all previously indexed geo points that comply the given {@link ShapeField.QueryRelation}
+ * with the specified array of {@link LatLonGeometry}.
+ *
+ * <p>The field must be indexed using {@link LatLonDocValuesField} added per document.
+ */
+class LatLonDocValuesQuery extends Query {
+
+  private final String field;
+  private final LatLonGeometry[] geometries;
+  private final ShapeField.QueryRelation queryRelation;
+  private final Component2D component2D;
+
+  LatLonDocValuesQuery(
+      String field, ShapeField.QueryRelation queryRelation, LatLonGeometry... geometries) {
+    if (field == null) {
+      throw new IllegalArgumentException("field must not be null");
+    }
+    if (queryRelation == null) {
+      throw new IllegalArgumentException("queryRelation must not be null");
+    }
+    if (queryRelation == ShapeField.QueryRelation.WITHIN) {
+      for (LatLonGeometry geometry : geometries) {
+        if (geometry instanceof Line) {
+          // TODO: line queries do not support within relations
+          throw new IllegalArgumentException(
+              "LatLonDocValuesPointQuery does not support "
+                  + ShapeField.QueryRelation.WITHIN
+                  + " queries with line geometries");
+        }
+      }
+    }
+    if (queryRelation == ShapeField.QueryRelation.CONTAINS) {
+      for (LatLonGeometry geometry : geometries) {
+        if ((geometry instanceof Point) == false) {
+          throw new IllegalArgumentException(
+              "LatLonDocValuesPointQuery does not support "
+                  + ShapeField.QueryRelation.CONTAINS
+                  + " queries with non-points geometries");
+        }
+      }
+    }
+    this.field = field;
+    this.geometries = geometries;
+    this.queryRelation = queryRelation;
+    this.component2D = LatLonGeometry.create(geometries);
+  }
+
+  @Override
+  public String toString(String field) {
+    StringBuilder sb = new StringBuilder();
+    if (!this.field.equals(field)) {
+      sb.append(this.field);
+      sb.append(':');
+    }
+    sb.append(queryRelation).append(':');
+    sb.append("geometries(").append(Arrays.toString(geometries));
+    return sb.append(")").toString();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (sameClassAs(obj) == false) {
+      return false;
+    }
+    LatLonDocValuesQuery other = (LatLonDocValuesQuery) obj;
+    return field.equals(other.field)
+        && queryRelation == other.queryRelation
+        && Arrays.equals(geometries, other.geometries);
+  }
+
+  @Override
+  public int hashCode() {
+    int h = classHash();
+    h = 31 * h + field.hashCode();
+    h = 31 * h + queryRelation.hashCode();
+    h = 31 * h + Arrays.hashCode(geometries);
+    return h;
+  }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
+  public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost)
+      throws IOException {
+    final GeoEncodingUtils.Component2DPredicate component2DPredicate =
+        queryRelation == ShapeField.QueryRelation.CONTAINS
+            ? null
+            : GeoEncodingUtils.createComponentPredicate(component2D);
+    return new ConstantScoreWeight(this, boost) {
+
+      @Override
+      public Scorer scorer(LeafReaderContext context) throws IOException {
+        final SortedNumericDocValues values = context.reader().getSortedNumericDocValues(field);
+        if (values == null) {
+          return null;
+        }
+        final TwoPhaseIterator iterator;
+        switch (queryRelation) {
+          case INTERSECTS:
+            iterator = intersects(values, component2DPredicate);
+            break;
+          case WITHIN:
+            iterator = within(values, component2DPredicate);
+            break;
+          case DISJOINT:
+            iterator = disjoint(values, component2DPredicate);
+            break;
+          case CONTAINS:
+            iterator = contains(values, geometries);
+            break;
+          default:
+            throw new IllegalArgumentException(
+                "Invalid query relationship:[" + queryRelation + "]");
+        }
+        return new ConstantScoreScorer(this, boost, scoreMode, iterator);
+      }
+
+      @Override
+      public boolean isCacheable(LeafReaderContext ctx) {
+        return DocValues.isCacheable(ctx, field);
+      }
+    };
+  }
+
+  private TwoPhaseIterator intersects(
+      SortedNumericDocValues values, GeoEncodingUtils.Component2DPredicate component2DPredicate) {
+    return new TwoPhaseIterator(values) {
+      @Override
+      public boolean matches() throws IOException {
+        for (int i = 0, count = values.docValueCount(); i < count; ++i) {
+          final long value = values.nextValue();
+          final int lat = (int) (value >>> 32);
+          final int lon = (int) (value & 0xFFFFFFFF);
+          if (component2DPredicate.test(lat, lon)) {
+            return true;
+          }
+        }
+        return false;
+      }
+
+      @Override
+      public float matchCost() {
+        return 1000f; // TODO: what should it be?
+      }
+    };
+  }
+
+  private TwoPhaseIterator within(
+      SortedNumericDocValues values, GeoEncodingUtils.Component2DPredicate component2DPredicate) {
+    return new TwoPhaseIterator(values) {
+      @Override
+      public boolean matches() throws IOException {
+        for (int i = 0, count = values.docValueCount(); i < count; ++i) {
+          final long value = values.nextValue();
+          final int lat = (int) (value >>> 32);
+          final int lon = (int) (value & 0xFFFFFFFF);
+          if (component2DPredicate.test(lat, lon) == false) {
+            return false;
+          }
+        }
+        return true;
+      }
+
+      @Override
+      public float matchCost() {
+        return 1000f; // TODO: what should it be?
+      }
+    };
+  }
+
+  private TwoPhaseIterator disjoint(
+      SortedNumericDocValues values, GeoEncodingUtils.Component2DPredicate component2DPredicate) {
+    return new TwoPhaseIterator(values) {
+      @Override
+      public boolean matches() throws IOException {
+        for (int i = 0, count = values.docValueCount(); i < count; ++i) {
+          final long value = values.nextValue();
+          final int lat = (int) (value >>> 32);
+          final int lon = (int) (value & 0xFFFFFFFF);
+          if (component2DPredicate.test(lat, lon)) {
+            return false;
+          }
+        }
+        return true;
+      }
+
+      @Override
+      public float matchCost() {
+        return 1000f; // TODO: what should it be?
+      }
+    };
+  }
+
+  private TwoPhaseIterator contains(SortedNumericDocValues values, LatLonGeometry[] geometries) {
+    final List<Component2D> component2Ds = new ArrayList<>(geometries.length);
+    for (int i = 0; i < geometries.length; i++) {
+      component2Ds.add(LatLonGeometry.create(geometries[i]));
+    }
+    return new TwoPhaseIterator(values) {
+      @Override
+      public boolean matches() throws IOException {
+        Component2D.WithinRelation answer = Component2D.WithinRelation.DISJOINT;
+        for (int i = 0, count = values.docValueCount(); i < count; ++i) {
+          final long value = values.nextValue();
+          final double lat = GeoEncodingUtils.decodeLatitude((int) (value >>> 32));
+          final double lon = GeoEncodingUtils.decodeLongitude((int) (value & 0xFFFFFFFF));
+          for (Component2D component2D : component2Ds) {
+            Component2D.WithinRelation relation = component2D.withinPoint(lon, lat);
+            if (relation == Component2D.WithinRelation.NOTWITHIN) {
+              return false;
+            } else if (relation != Component2D.WithinRelation.DISJOINT) {
+              answer = relation;
+            }
+          }
+        }
+        return answer == Component2D.WithinRelation.CANDIDATE;
+      }
+
+      @Override
+      public float matchCost() {
+        return 1000f; // TODO: what should it be?
+      }
+    };
+  }
+}
diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java b/lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java
index 7dfb5a0..2ad1393 100644
--- a/lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java
+++ b/lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java
@@ -16,15 +16,23 @@
  */
 package org.apache.lucene.document;
 
+import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
+import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
+import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
+import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitudeCeil;
+import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
+import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
+
 import org.apache.lucene.geo.Circle;
 import org.apache.lucene.geo.LatLonGeometry;
+import org.apache.lucene.geo.Point;
 import org.apache.lucene.geo.Polygon;
 import org.apache.lucene.geo.Rectangle;
 import org.apache.lucene.index.FieldInfo;
 import org.apache.lucene.index.PointValues;
 import org.apache.lucene.search.BooleanClause;
-import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.BooleanClause.Occur;
+import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.BoostQuery;
 import org.apache.lucene.search.ConstantScoreQuery;
 import org.apache.lucene.search.MatchNoDocsQuery;
@@ -33,56 +41,58 @@ import org.apache.lucene.search.Query;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.NumericUtils;
 
-import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitudeCeil;
-import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
-
-/** 
+/**
  * An indexed location field.
- * <p>
- * Finding all documents within a range at search time is
- * efficient.  Multiple values for the same field in one document
- * is allowed. 
- * <p>
- * This field defines static factory methods for common operations:
+ *
+ * <p>Finding all documents within a range at search time is efficient. Multiple values for the same
+ * field in one document is allowed.
+ *
+ * <p>This field defines static factory methods for common operations:
+ *
  * <ul>
  *   <li>{@link #newBoxQuery newBoxQuery()} for matching points within a bounding box.
- *   <li>{@link #newDistanceQuery newDistanceQuery()} for matching points within a specified distance.
+ *   <li>{@link #newDistanceQuery newDistanceQuery()} for matching points within a specified
+ *       distance.
  *   <li>{@link #newPolygonQuery newPolygonQuery()} for matching points within an arbitrary polygon.
- *   <li>{@link #newGeometryQuery newGeometryQuery()} for matching points within an arbitrary geometry collection.
+ *   <li>{@link #newGeometryQuery newGeometryQuery()} for matching points within an arbitrary
+ *       geometry collection.
  * </ul>
- * <p>
- * If you also need per-document operations such as sort by distance, add a separate {@link LatLonDocValuesField} instance.
- * If you also need to store the value, you should add a separate {@link StoredField} instance.
- * <p>
- * <b>WARNING</b>: Values are indexed with some loss of precision from the
- * original {@code double} values (4.190951585769653E-8 for the latitude component
- * and 8.381903171539307E-8 for longitude).
+ *
+ * <p>If you also need per-document operations such as sort by distance, add a separate {@link
+ * LatLonDocValuesField} instance. If you also need to store the value, you should add a separate
+ * {@link StoredField} instance.
+ *
+ * <p><b>WARNING</b>: Values are indexed with some loss of precision from the original {@code
+ * double} values (4.190951585769653E-8 for the latitude component and 8.381903171539307E-8 for
+ * longitude).
+ *
  * @see PointValues
  * @see LatLonDocValuesField
  */
-// TODO ^^^ that is very sandy and hurts the API, usage, and tests tremendously, because what the user passes
-// to the field is not actually what gets indexed. Float would be 1E-5 error vs 1E-7, but it might be
-// a better tradeoff? then it would be completely transparent to the user and lucene would be "lossless".
+// TODO ^^^ that is very sandy and hurts the API, usage, and tests tremendously, because what the
+// user passes
+// to the field is not actually what gets indexed. Float would be 1E-5 error vs 1E-7, but it might
+// be
+// a better tradeoff? then it would be completely transparent to the user and lucene would be
+// "lossless".
 public class LatLonPoint extends Field {
   /** LatLonPoint is encoded as integer values so number of bytes is 4 */
   public static final int BYTES = Integer.BYTES;
   /**
    * Type for an indexed LatLonPoint
-   * <p>
-   * Each point stores two dimensions with 4 bytes per dimension.
+   *
+   * <p>Each point stores two dimensions with 4 bytes per dimension.
    */
   public static final FieldType TYPE = new FieldType();
+
   static {
     TYPE.setDimensions(2, Integer.BYTES);
     TYPE.freeze();
   }
-  
+
   /**
    * Change the values of this field
+   *
    * @param latitude latitude value: must be within standard +/-90 coordinate bounds.
    * @param longitude longitude value: must be within standard +/-180 coordinate bounds.
    * @throws IllegalArgumentException if latitude or longitude are out of bounds
@@ -103,12 +113,14 @@ public class LatLonPoint extends Field {
     NumericUtils.intToSortableBytes(longitudeEncoded, bytes, Integer.BYTES);
   }
 
-  /** 
+  /**
    * Creates a new LatLonPoint with the specified latitude and longitude
+   *
    * @param name field name
    * @param latitude latitude value: must be within standard +/-90 coordinate bounds.
    * @param longitude longitude value: must be within standard +/-180 coordinate bounds.
-   * @throws IllegalArgumentException if the field name is null or latitude or longitude are out of bounds
+   * @throws IllegalArgumentException if the field name is null or latitude or longitude are out of
+   *     bounds
    */
   public LatLonPoint(String name, double latitude, double longitude) {
     super(name, TYPE);
@@ -131,7 +143,7 @@ public class LatLonPoint extends Field {
     result.append('>');
     return result.toString();
   }
-  
+
   /** sugar encodes a single point as a byte array */
   private static byte[] encode(double latitude, double longitude) {
     byte[] bytes = new byte[2 * Integer.BYTES];
@@ -139,7 +151,7 @@ public class LatLonPoint extends Field {
     NumericUtils.intToSortableBytes(encodeLongitude(longitude), bytes, Integer.BYTES);
     return bytes;
   }
-  
+
   /** sugar encodes a single point as a byte array, rounding values up */
   private static byte[] encodeCeil(double latitude, double longitude) {
     byte[] bytes = new byte[2 * Integer.BYTES];
@@ -150,16 +162,28 @@ public class LatLonPoint extends Field {
 
   /** helper: checks a fieldinfo and throws exception if its definitely not a LatLonPoint */
   static void checkCompatible(FieldInfo fieldInfo) {
-    // point/dv properties could be "unset", if you e.g. used only StoredField with this same name in the segment.
-    if (fieldInfo.getPointDimensionCount() != 0 && fieldInfo.getPointDimensionCount() != TYPE.pointDimensionCount()) {
-      throw new IllegalArgumentException("field=\"" + fieldInfo.name + "\" was indexed with numDims=" + fieldInfo.getPointDimensionCount() +
-          " but this point type has numDims=" + TYPE.pointDimensionCount() +
-                                         ", is the field really a LatLonPoint?");
+    // point/dv properties could be "unset", if you e.g. used only StoredField with this same name
+    // in the segment.
+    if (fieldInfo.getPointDimensionCount() != 0
+            && fieldInfo.getPointDimensionCount() != TYPE.pointDimensionCount()) {
+      throw new IllegalArgumentException(
+              "field=\""
+                      + fieldInfo.name
+                      + "\" was indexed with numDims="
+                      + fieldInfo.getPointDimensionCount()
+                      + " but this point type has numDims="
+                      + TYPE.pointDimensionCount()
+                      + ", is the field really a LatLonPoint?");
     }
     if (fieldInfo.getPointNumBytes() != 0 && fieldInfo.getPointNumBytes() != TYPE.pointNumBytes()) {
-      throw new IllegalArgumentException("field=\"" + fieldInfo.name + "\" was indexed with bytesPerDim=" + fieldInfo.getPointNumBytes() + 
-                                         " but this point type has bytesPerDim=" + TYPE.pointNumBytes() + 
-                                         ", is the field really a LatLonPoint?");
+      throw new IllegalArgumentException(
+              "field=\""
+                      + fieldInfo.name
+                      + "\" was indexed with bytesPerDim="
+                      + fieldInfo.getPointNumBytes()
+                      + " but this point type has bytesPerDim="
+                      + TYPE.pointNumBytes()
+                      + ", is the field really a LatLonPoint?");
     }
   }
 
@@ -167,8 +191,9 @@ public class LatLonPoint extends Field {
 
   /**
    * Create a query for matching a bounding box.
-   * <p>
-   * The box may cross over the dateline.
+   *
+   * <p>The box may cross over the dateline.
+   *
    * @param field field name. must not be null.
    * @param minLatitude latitude lower bound: must be within standard +/-90 coordinate bounds.
    * @param maxLatitude latitude upper bound: must be within standard +/-90 coordinate bounds.
@@ -177,9 +202,16 @@ public class LatLonPoint extends Field {
    * @return query matching points within this box
    * @throws IllegalArgumentException if {@code field} is null, or the box has invalid coordinates.
    */
-  public static Query newBoxQuery(String field, double minLatitude, double maxLatitude, double minLongitude, double maxLongitude) {
-    // exact double values of lat=90.0D and lon=180.0D must be treated special as they are not represented in the encoding
-    // and should not drag in extra bogus junk! TODO: should encodeCeil just throw ArithmeticException to be less trappy here?
+  public static Query newBoxQuery(
+          String field,
+          double minLatitude,
+          double maxLatitude,
+          double minLongitude,
+          double maxLongitude) {
+    // exact double values of lat=90.0D and lon=180.0D must be treated special as they are not
+    // represented in the encoding
+    // and should not drag in extra bogus junk! TODO: should encodeCeil just throw
+    // ArithmeticException to be less trappy here?
     if (minLatitude == 90.0) {
       // range cannot match as 90.0 can never exist
       return new MatchNoDocsQuery("LatLonPoint.newBoxQuery with minLatitude=90.0");
@@ -197,7 +229,8 @@ public class LatLonPoint extends Field {
     byte[] upper = encode(maxLatitude, maxLongitude);
     // Crosses date line: we just rewrite into OR of two bboxes, with longitude as an open range:
     if (maxLongitude < minLongitude) {
-      // Disable coord here because a multi-valued doc could match both rects and get unfairly boosted:
+      // Disable coord here because a multi-valued doc could match both rects and get unfairly
+      // boosted:
       BooleanQuery.Builder q = new BooleanQuery.Builder();
 
       // E.g.: maxLon = -179, minLon = 179
@@ -217,7 +250,7 @@ public class LatLonPoint extends Field {
       return newBoxInternal(field, lower, upper);
     }
   }
-  
+
   private static Query newBoxInternal(String field, byte[] min, byte[] max) {
     return new PointRangeQuery(field, min, max, 2) {
       @Override
@@ -232,22 +265,27 @@ public class LatLonPoint extends Field {
       }
     };
   }
-  
+
   /**
    * Create a query for matching points within the specified distance of the supplied location.
+   *
    * @param field field name. must not be null.
    * @param latitude latitude at the center: must be within standard +/-90 coordinate bounds.
    * @param longitude longitude at the center: must be within standard +/-180 coordinate bounds.
-   * @param radiusMeters maximum distance from the center in meters: must be non-negative and finite.
+   * @param radiusMeters maximum distance from the center in meters: must be non-negative and
+   *     finite.
    * @return query matching points within this distance
-   * @throws IllegalArgumentException if {@code field} is null, location has invalid coordinates, or radius is invalid.
+   * @throws IllegalArgumentException if {@code field} is null, location has invalid coordinates, or
+   *     radius is invalid.
    */
-  public static Query newDistanceQuery(String field, double latitude, double longitude, double radiusMeters) {
+  public static Query newDistanceQuery(
+          String field, double latitude, double longitude, double radiusMeters) {
     return new LatLonPointDistanceQuery(field, latitude, longitude, radiusMeters);
   }
-  
-  /** 
+
+  /**
    * Create a query for matching one or more polygons.
+   *
    * @param field field name. must not be null.
    * @param polygons array of polygons. must not be null or empty
    * @return query matching points within this polygon
@@ -255,19 +293,25 @@ public class LatLonPoint extends Field {
    * @see Polygon
    */
   public static Query newPolygonQuery(String field, Polygon... polygons) {
-    return newGeometryQuery(field, polygons);
+    return newGeometryQuery(field, ShapeField.QueryRelation.INTERSECTS, polygons);
   }
 
   /**
-   * Create a query for matching one or more geometries. Line geometries are not supported.
+   * Create a query for matching one or more geometries against the provided {@link
+   * ShapeField.QueryRelation}. Line geometries are not supported for WITHIN relationship.
+   *
    * @param field field name. must not be null.
+   * @param queryRelation The relation the points needs to satisfy with the provided geometries,
+   *     must not be null.
    * @param latLonGeometries array of LatLonGeometries. must not be null or empty.
    * @return query matching points within at least one geometry.
-   * @throws IllegalArgumentException  if {@code field} is null, {@code latLonGeometries} is null, empty or contain a null or line geometry.
+   * @throws IllegalArgumentException if {@code field} is null, {@code queryRelation} is null,
+   *     {@code latLonGeometries} is null, empty or contain a null.
    * @see LatLonGeometry
    */
-  public static Query newGeometryQuery(String field, LatLonGeometry... latLonGeometries) {
-    if (latLonGeometries.length == 1) {
+  public static Query newGeometryQuery(
+          String field, ShapeField.QueryRelation queryRelation, LatLonGeometry... latLonGeometries) {
+    if (queryRelation == ShapeField.QueryRelation.INTERSECTS && latLonGeometries.length == 1) {
       if (latLonGeometries[0] instanceof Rectangle) {
         final Rectangle rect = (Rectangle) latLonGeometries[0];
         return newBoxQuery(field, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
@@ -277,29 +321,45 @@ public class LatLonPoint extends Field {
         return newDistanceQuery(field, circle.getLat(), circle.getLon(), circle.getRadius());
       }
     }
-    return new LatLonPointInGeometryQuery(field, latLonGeometries);
+    if (queryRelation == ShapeField.QueryRelation.CONTAINS) {
+      return makeContainsGeometryQuery(field, latLonGeometries);
+    }
+    return new LatLonPointQuery(field, queryRelation, latLonGeometries);
+  }
+
+  private static Query makeContainsGeometryQuery(String field, LatLonGeometry... latLonGeometries) {
+    BooleanQuery.Builder builder = new BooleanQuery.Builder();
+    for (LatLonGeometry geometry : latLonGeometries) {
+      if ((geometry instanceof Point) == false) {
+        return new MatchNoDocsQuery(
+                "Contains LatLonPoint.newGeometryQuery with non-point geometries");
+      }
+      builder.add(
+              new LatLonPointQuery(field, ShapeField.QueryRelation.CONTAINS, geometry),
+              BooleanClause.Occur.MUST);
+    }
+    return new ConstantScoreQuery(builder.build());
   }
 
   /**
-   * Given a field that indexes point values into a {@link LatLonPoint}
-   * and doc values into {@link LatLonDocValuesField}, this returns a query that scores
-   * documents based on their haversine distance in meters to {@code (originLat, originLon)}:
-   * {@code score = weight * pivotDistanceMeters / (pivotDistanceMeters + distance)}, ie.
-   * score is in the {@code [0, weight]} range, is equal to {@code weight} when
-   * the document's value is equal to {@code (originLat, originLon)} and is equal to
-   * {@code weight/2}  when the document's value is distant of
-   * {@code pivotDistanceMeters} from {@code (originLat, originLon)}.
-   * In case of multi-valued fields, only the closest point to {@code (originLat, originLon)}
-   * will be considered.
-   * This query is typically useful to boost results based on distance by adding
-   * this query to a {@link Occur#SHOULD} clause of a {@link BooleanQuery}.
+   * Given a field that indexes point values into a {@link LatLonPoint} and doc values into {@link
+   * LatLonDocValuesField}, this returns a query that scores documents based on their haversine
+   * distance in meters to {@code (originLat, originLon)}: {@code score = weight *
+   * pivotDistanceMeters / (pivotDistanceMeters + distance)}, ie. score is in the {@code [0,
+   * weight]} range, is equal to {@code weight} when the document's value is equal to {@code
+   * (originLat, originLon)} and is equal to {@code weight/2} when the document's value is distant
+   * of {@code pivotDistanceMeters} from {@code (originLat, originLon)}. In case of multi-valued
+   * fields, only the closest point to {@code (originLat, originLon)} will be considered. This query
+   * is typically useful to boost results based on distance by adding this query to a {@link
+   * Occur#SHOULD} clause of a {@link BooleanQuery}.
    */
-  public static Query newDistanceFeatureQuery(String field, float weight, double originLat, double originLon, double pivotDistanceMeters) {
-    Query query = new LatLonPointDistanceFeatureQuery(field, originLat, originLon, pivotDistanceMeters);
+  public static Query newDistanceFeatureQuery(
+          String field, float weight, double originLat, double originLon, double pivotDistanceMeters) {
+    Query query =
+            new LatLonPointDistanceFeatureQuery(field, originLat, originLon, pivotDistanceMeters);
     if (weight != 1f) {
       query = new BoostQuery(query, weight);
     }
     return query;
   }
-
 }
diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonPointInGeometryQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonPointInGeometryQuery.java
deleted file mode 100644
index d0c831e..0000000
--- a/lucene/core/src/java/org/apache/lucene/document/LatLonPointInGeometryQuery.java
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.lucene.document;
-
-import org.apache.lucene.geo.Component2D;
-import org.apache.lucene.geo.GeoEncodingUtils;
-import org.apache.lucene.geo.LatLonGeometry;
-import org.apache.lucene.geo.Line;
-import org.apache.lucene.index.FieldInfo;
-import org.apache.lucene.index.LeafReader;
-import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.PointValues;
-import org.apache.lucene.index.PointValues.IntersectVisitor;
-import org.apache.lucene.index.PointValues.Relation;
-import org.apache.lucene.search.ConstantScoreScorer;
-import org.apache.lucene.search.ConstantScoreWeight;
-import org.apache.lucene.search.DocIdSetIterator;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.QueryVisitor;
-import org.apache.lucene.search.ScoreMode;
-import org.apache.lucene.search.Scorer;
-import org.apache.lucene.search.ScorerSupplier;
-import org.apache.lucene.search.Weight;
-import org.apache.lucene.util.DocIdSetBuilder;
-import org.apache.lucene.util.FutureArrays;
-import org.apache.lucene.util.NumericUtils;
-
-import java.io.IOException;
-import java.util.Arrays;
-
-import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
-
-/** Finds all previously indexed points that fall within the specified geometries.
- *
- *  <p>The field must be indexed with using {@link LatLonPoint} added per document.
- *
- *  @lucene.experimental */
-
-final class LatLonPointInGeometryQuery extends Query {
-  final String field;
-  final LatLonGeometry[] geometries;
-
-  LatLonPointInGeometryQuery(String field, LatLonGeometry[] geometries) {
-    if (field == null) {
-      throw new IllegalArgumentException("field must not be null");
-    }
-    if (geometries == null) {
-      throw new IllegalArgumentException("geometries must not be null");
-    }
-    if (geometries.length == 0) {
-      throw new IllegalArgumentException("geometries must not be empty");
-    }
-    for (int i = 0; i < geometries.length; i++) {
-      if (geometries[i] == null) {
-        throw new IllegalArgumentException("geometries[" + i + "] must not be null");
-      }
-      if (geometries[i] instanceof Line) {
-        throw new IllegalArgumentException("LatLonPointInGeometryQuery does not support queries with line geometries");
-      }
-    }
-    this.field = field;
-    this.geometries = geometries.clone();
-  }
-
-  @Override
-  public void visit(QueryVisitor visitor) {
-    if (visitor.acceptField(field)) {
-      visitor.visitLeaf(this);
-    }
-  }
-
-  private IntersectVisitor getIntersectVisitor(DocIdSetBuilder result, Component2D tree, GeoEncodingUtils.Component2DPredicate component2DPredicate,
-                                               byte[] minLat, byte[] maxLat, byte[] minLon, byte[] maxLon) {
-    return new IntersectVisitor() {
-          DocIdSetBuilder.BulkAdder adder;
-
-          @Override
-          public void grow(int count) {
-            adder = result.grow(count);
-          }
-
-          @Override
-          public void visit(int docID) {
-            adder.add(docID);
-          }
-
-          @Override
-          public void visit(int docID, byte[] packedValue) {
-            if (component2DPredicate.test(NumericUtils.sortableBytesToInt(packedValue, 0),
-                NumericUtils.sortableBytesToInt(packedValue, Integer.BYTES))) {
-              visit(docID);
-            }
-          }
-
-          @Override
-          public void visit(DocIdSetIterator iterator, byte[] packedValue) throws IOException {
-            if (component2DPredicate.test(NumericUtils.sortableBytesToInt(packedValue, 0),
-                NumericUtils.sortableBytesToInt(packedValue, Integer.BYTES))) {
-              int docID;
-              while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
-                visit(docID);
-              }
-            }
-          }
-
-          @Override
-          public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
-            if (FutureArrays.compareUnsigned(minPackedValue, 0, Integer.BYTES, maxLat, 0, Integer.BYTES) > 0 ||
-                FutureArrays.compareUnsigned(maxPackedValue, 0, Integer.BYTES, minLat, 0, Integer.BYTES) < 0 ||
-                FutureArrays.compareUnsigned(minPackedValue, Integer.BYTES, Integer.BYTES + Integer.BYTES, maxLon, 0, Integer.BYTES) > 0 ||
-                FutureArrays.compareUnsigned(maxPackedValue, Integer.BYTES, Integer.BYTES + Integer.BYTES, minLon, 0, Integer.BYTES) < 0) {
-              // outside of global bounding box range
-              return Relation.CELL_OUTSIDE_QUERY;
-            }
-
-            double cellMinLat = decodeLatitude(minPackedValue, 0);
-            double cellMinLon = decodeLongitude(minPackedValue, Integer.BYTES);
-            double cellMaxLat = decodeLatitude(maxPackedValue, 0);
-            double cellMaxLon = decodeLongitude(maxPackedValue, Integer.BYTES);
-
-            return tree.relate(cellMinLon, cellMaxLon, cellMinLat, cellMaxLat);
-          }
-        };
-  }
-
-  @Override
-  public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
-    final Component2D tree = LatLonGeometry.create(geometries);
-    if (tree.getMinY() > tree.getMaxY()) {
-      // encodeLatitudeCeil may cause minY to be > maxY iff
-      // the delta between the longitude < the encoding resolution
-      return new ConstantScoreWeight(this, boost) {
-        @Override
-        public Scorer scorer(LeafReaderContext context) {
-          return null;
-        }
-
-        @Override
-        public boolean isCacheable(LeafReaderContext ctx) {
-          return false;
-        }
-      };
-    }
-    final GeoEncodingUtils.Component2DPredicate component2DPredicate = GeoEncodingUtils.createComponentPredicate(tree);
-    // bounding box over all geometries, this can speed up tree intersection/cheaply improve approximation for complex multi-geometries
-    final byte minLat[] = new byte[Integer.BYTES];
-    final byte maxLat[] = new byte[Integer.BYTES];
-    final byte minLon[] = new byte[Integer.BYTES];
-    final byte maxLon[] = new byte[Integer.BYTES];
-    NumericUtils.intToSortableBytes(encodeLatitude(tree.getMinY()), minLat, 0);
-    NumericUtils.intToSortableBytes(encodeLatitude(tree.getMaxY()), maxLat, 0);
-    NumericUtils.intToSortableBytes(encodeLongitude(tree.getMinX()), minLon, 0);
-    NumericUtils.intToSortableBytes(encodeLongitude(tree.getMaxX()), maxLon, 0);
-
-    return new ConstantScoreWeight(this, boost) {
-
-      @Override
-      public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
-        LeafReader reader = context.reader();
-        PointValues values = reader.getPointValues(field);
-        if (values == null) {
-          // No docs in this segment had any points fields
-          return null;
-        }
-        FieldInfo fieldInfo = reader.getFieldInfos().fieldInfo(field);
-        if (fieldInfo == null) {
-          // No docs in this segment indexed this field at all
-          return null;
-        }
-        LatLonPoint.checkCompatible(fieldInfo);
-        final Weight weight = this;
-
-        return new ScorerSupplier() {
-
-          long cost = -1;
-          DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc(), values, field);
-          final IntersectVisitor visitor = getIntersectVisitor(result, tree, component2DPredicate, minLat, maxLat, minLon, maxLon);
-
-          @Override
-          public Scorer get(long leadCost) throws IOException {
-            values.intersect(visitor);
-            return new ConstantScoreScorer(weight, score(), scoreMode, result.build().iterator());
-          }
-
-          @Override
-          public long cost() {
-            if (cost == -1) {
-               // Computing the cost may be expensive, so only do it if necessary
-              cost = values.estimateDocCount(visitor);
-              assert cost >= 0;
-            }
-            return cost;
-          }
-        };
-      }
-
-      @Override
-      public Scorer scorer(LeafReaderContext context) throws IOException {
-        ScorerSupplier scorerSupplier = scorerSupplier(context);
-        if (scorerSupplier == null) {
-          return null;
-        }
-        return scorerSupplier.get(Long.MAX_VALUE);
-      }
-
-      @Override
-      public boolean isCacheable(LeafReaderContext ctx) {
-        return true;
-      }
-    };
-
-  }
-
-  /** Returns the query field */
-  public String getField() {
-    return field;
-  }
-
-  /** Returns a copy of the internal geometry array */
-  public LatLonGeometry[] getGeometries() {
-    return geometries.clone();
-  }
-
-  @Override
-  public int hashCode() {
-    final int prime = 31;
-    int result = classHash();
-    result = prime * result + field.hashCode();
-    result = prime * result + Arrays.hashCode(geometries);
-    return result;
-  }
-
-  @Override
-  public boolean equals(Object other) {
-    return sameClassAs(other) &&
-           equalsTo(getClass().cast(other));
-  }
-
-  private boolean equalsTo(LatLonPointInGeometryQuery other) {
-    return field.equals(other.field) &&
-           Arrays.equals(geometries, other.geometries);
-  }
-
-  @Override
-  public String toString(String field) {
-    final StringBuilder sb = new StringBuilder();
-    sb.append(getClass().getSimpleName());
-    sb.append(':');
-    if (this.field.equals(field) == false) {
-      sb.append(" field=");
-      sb.append(this.field);
-      sb.append(':');
-    }
-    sb.append(Arrays.toString(geometries));
-    return sb.toString();
-  }
-}
diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonPointQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonPointQuery.java
new file mode 100644
index 0000000..2b9bcd4
--- /dev/null
+++ b/lucene/core/src/java/org/apache/lucene/document/LatLonPointQuery.java
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.document;
+
+import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
+import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
+import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
+import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
+
+import java.util.Arrays;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import org.apache.lucene.document.ShapeField.QueryRelation;
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.GeoEncodingUtils;
+import org.apache.lucene.geo.LatLonGeometry;
+import org.apache.lucene.geo.Line;
+import org.apache.lucene.geo.Point;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.NumericUtils;
+
+/**
+ * Finds all previously indexed geo points that comply the given {@link QueryRelation} with the
+ * specified array of {@link LatLonGeometry}.
+ *
+ * <p>The field must be indexed using one or more {@link LatLonPoint} added per document.
+ */
+final class LatLonPointQuery extends SpatialQuery {
+  private final LatLonGeometry[] geometries;
+  private final Component2D component2D;
+
+  /**
+   * Creates a query that matches all indexed shapes to the provided array of {@link LatLonGeometry}
+   */
+  LatLonPointQuery(String field, QueryRelation queryRelation, LatLonGeometry... geometries) {
+    super(field, queryRelation);
+    if (queryRelation == QueryRelation.WITHIN) {
+      for (LatLonGeometry geometry : geometries) {
+        if (geometry instanceof Line) {
+          // TODO: line queries do not support within relations
+          throw new IllegalArgumentException(
+              "LatLonPointQuery does not support "
+                  + QueryRelation.WITHIN
+                  + " queries with line geometries");
+        }
+      }
+    }
+    if (queryRelation == ShapeField.QueryRelation.CONTAINS) {
+      for (LatLonGeometry geometry : geometries) {
+        if ((geometry instanceof Point) == false) {
+          throw new IllegalArgumentException(
+              "LatLonPointQuery does not support "
+                  + ShapeField.QueryRelation.CONTAINS
+                  + " queries with non-points geometries");
+        }
+      }
+    }
+    this.component2D = LatLonGeometry.create(geometries);
+    this.geometries = geometries.clone();
+  }
+
+  @Override
+  protected SpatialVisitor getSpatialVisitor() {
+    final GeoEncodingUtils.Component2DPredicate component2DPredicate =
+        GeoEncodingUtils.createComponentPredicate(component2D);
+    // bounding box over all geometries, this can speed up tree intersection/cheaply improve
+    // approximation for complex multi-geometries
+    final byte[] minLat = new byte[Integer.BYTES];
+    final byte[] maxLat = new byte[Integer.BYTES];
+    final byte[] minLon = new byte[Integer.BYTES];
+    final byte[] maxLon = new byte[Integer.BYTES];
+    NumericUtils.intToSortableBytes(encodeLatitude(component2D.getMinY()), minLat, 0);
+    NumericUtils.intToSortableBytes(encodeLatitude(component2D.getMaxY()), maxLat, 0);
+    NumericUtils.intToSortableBytes(encodeLongitude(component2D.getMinX()), minLon, 0);
+    NumericUtils.intToSortableBytes(encodeLongitude(component2D.getMaxX()), maxLon, 0);
+
+    return new SpatialVisitor() {
+      @Override
+      protected Relation relate(byte[] minPackedValue, byte[] maxPackedValue) {
+        if (FutureArrays.compareUnsigned(minPackedValue, 0, Integer.BYTES, maxLat, 0, Integer.BYTES) > 0
+            || FutureArrays.compareUnsigned(maxPackedValue, 0, Integer.BYTES, minLat, 0, Integer.BYTES)
+                < 0
+            || FutureArrays.compareUnsigned(
+                    minPackedValue,
+                    Integer.BYTES,
+                    Integer.BYTES + Integer.BYTES,
+                    maxLon,
+                    0,
+                    Integer.BYTES)
+                > 0
+            || FutureArrays.compareUnsigned(
+                    maxPackedValue,
+                    Integer.BYTES,
+                    Integer.BYTES + Integer.BYTES,
+                    minLon,
+                    0,
+                    Integer.BYTES)
+                < 0) {
+          // outside of global bounding box range
+          return Relation.CELL_OUTSIDE_QUERY;
+        }
+
+        double cellMinLat = decodeLatitude(minPackedValue, 0);
+        double cellMinLon = decodeLongitude(minPackedValue, Integer.BYTES);
+        double cellMaxLat = decodeLatitude(maxPackedValue, 0);
+        double cellMaxLon = decodeLongitude(maxPackedValue, Integer.BYTES);
+
+        return component2D.relate(cellMinLon, cellMaxLon, cellMinLat, cellMaxLat);
+      }
+
+      @Override
+      protected Predicate<byte[]> intersects() {
+        return packedValue ->
+            component2DPredicate.test(
+                NumericUtils.sortableBytesToInt(packedValue, 0),
+                NumericUtils.sortableBytesToInt(packedValue, Integer.BYTES));
+      }
+
+      @Override
+      protected Predicate<byte[]> within() {
+        return packedValue ->
+            component2DPredicate.test(
+                NumericUtils.sortableBytesToInt(packedValue, 0),
+                NumericUtils.sortableBytesToInt(packedValue, Integer.BYTES));
+      }
+
+      @Override
+      protected Function<byte[], Component2D.WithinRelation> contains() {
+        return packedValue ->
+            component2D.withinPoint(
+                GeoEncodingUtils.decodeLongitude(
+                    NumericUtils.sortableBytesToInt(packedValue, Integer.BYTES)),
+                GeoEncodingUtils.decodeLatitude(NumericUtils.sortableBytesToInt(packedValue, 0)));
+      }
+    };
+  }
+
+  @Override
+  public String toString(String field) {
+    final StringBuilder sb = new StringBuilder();
+    sb.append(getClass().getSimpleName());
+    sb.append(':');
+    if (this.field.equals(field) == false) {
+      sb.append(" field=");
+      sb.append(this.field);
+      sb.append(':');
+    }
+    sb.append("[");
+    for (int i = 0; i < geometries.length; i++) {
+      sb.append(geometries[i].toString());
+      sb.append(',');
+    }
+    sb.append(']');
+    return sb.toString();
+  }
+
+  @Override
+  protected boolean equalsTo(Object o) {
+    return super.equalsTo(o) && Arrays.equals(geometries, ((LatLonPointQuery) o).geometries);
+  }
+
+  @Override
+  public int hashCode() {
+    int hash = super.hashCode();
+    hash = 31 * hash + Arrays.hashCode(geometries);
+    return hash;
+  }
+}
diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java
index be3c7d3..5434958 100644
--- a/lucene/core/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java
@@ -16,14 +16,6 @@
  */
 package org.apache.lucene.document;
 
-import org.apache.lucene.document.ShapeField.QueryRelation;
-import org.apache.lucene.geo.Component2D;
-import org.apache.lucene.geo.GeoUtils;
-import org.apache.lucene.geo.Rectangle;
-import org.apache.lucene.index.PointValues.Relation;
-import org.apache.lucene.util.FutureArrays;
-import org.apache.lucene.util.NumericUtils;
-
 import static java.lang.Integer.BYTES;
 import static org.apache.lucene.geo.GeoEncodingUtils.MAX_LON_ENCODED;
 import static org.apache.lucene.geo.GeoEncodingUtils.MIN_LON_ENCODED;
@@ -32,117 +24,182 @@ import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitudeCeil;
 import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
 import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
 
+import java.util.function.Function;
+import java.util.function.Predicate;
+import org.apache.lucene.document.ShapeField.QueryRelation;
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.GeoUtils;
+import org.apache.lucene.geo.Rectangle;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.util.FutureArrays;
+import org.apache.lucene.util.NumericUtils;
+
 /**
  * Finds all previously indexed geo shapes that intersect the specified bounding box.
  *
- * <p>The field must be indexed using
- * {@link org.apache.lucene.document.LatLonShape#createIndexableFields} added per document.
- **/
-final class LatLonShapeBoundingBoxQuery extends ShapeQuery {
+ * <p>The field must be indexed using {@link
+ * org.apache.lucene.document.LatLonShape#createIndexableFields} added per document.
+ */
+final class LatLonShapeBoundingBoxQuery extends SpatialQuery {
   private final Rectangle rectangle;
-  private final EncodedRectangle encodedRectangle;
 
   LatLonShapeBoundingBoxQuery(String field, QueryRelation queryRelation, Rectangle rectangle) {
     super(field, queryRelation);
     this.rectangle = rectangle;
-    this.encodedRectangle = new EncodedRectangle(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon);
-  }
-
-  @Override
-  protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle,
-                                            int maxXOffset, int maxYOffset, byte[] maxTriangle) {
-    if (queryRelation == QueryRelation.INTERSECTS || queryRelation == QueryRelation.DISJOINT) {
-      return encodedRectangle.intersectRangeBBox(minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle);
-    }
-    return encodedRectangle.relateRangeBBox(minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle);
-  }
-
-
-  @Override
-  protected boolean queryIntersects(byte[] t, ShapeField.DecodedTriangle scratchTriangle) {
-    ShapeField.decodeTriangle(t, scratchTriangle);
-
-    switch (scratchTriangle.type) {
-      case POINT: {
-        return encodedRectangle.contains(scratchTriangle.aX, scratchTriangle.aY);
-      }
-      case LINE: {
-        int aY = scratchTriangle.aY;
-        int aX = scratchTriangle.aX;
-        int bY = scratchTriangle.bY;
-        int bX = scratchTriangle.bX;
-        return encodedRectangle.intersectsLine(aX, aY, bX, bY);
-      }
-      case TRIANGLE: {
-        int aY = scratchTriangle.aY;
-        int aX = scratchTriangle.aX;
-        int bY = scratchTriangle.bY;
-        int bX = scratchTriangle.bX;
-        int cY = scratchTriangle.cY;
-        int cX = scratchTriangle.cX;
-        return encodedRectangle.intersectsTriangle(aX, aY, bX, bY, cX, cY);
-      }
-      default: throw new IllegalArgumentException("Unsupported triangle type :[" + scratchTriangle.type + "]");
-    }
-  }
-
-  @Override
-  protected boolean queryContains(byte[] t, ShapeField.DecodedTriangle scratchTriangle) {
-    ShapeField.decodeTriangle(t, scratchTriangle);
-
-    switch (scratchTriangle.type) {
-      case POINT: {
-        return encodedRectangle.contains(scratchTriangle.aX, scratchTriangle.aY);
-      }
-      case LINE: {
-        int aY = scratchTriangle.aY;
-        int aX = scratchTriangle.aX;
-        int bY = scratchTriangle.bY;
-        int bX = scratchTriangle.bX;
-        return encodedRectangle.containsLine(aX, aY, bX, bY);
-      }
-      case TRIANGLE: {
-        int aY = scratchTriangle.aY;
-        int aX = scratchTriangle.aX;
-        int bY = scratchTriangle.bY;
-        int bX = scratchTriangle.bX;
-        int cY = scratchTriangle.cY;
-        int cX = scratchTriangle.cX;
-        return encodedRectangle.containsTriangle(aX, aY, bX, bY, cX, cY);
-      }
-      default: throw new IllegalArgumentException("Unsupported triangle type :[" + scratchTriangle.type + "]");
-    }
   }
 
   @Override
-  protected Component2D.WithinRelation queryWithin(byte[] t, ShapeField.DecodedTriangle scratchTriangle) {
-    if (encodedRectangle.crossesDateline()) {
-      throw new IllegalArgumentException("withinTriangle is not supported for rectangles crossing the date line");
-    }
-    // decode indexed triangle
-    ShapeField.decodeTriangle(t, scratchTriangle);
-
-    switch (scratchTriangle.type) {
-      case POINT: {
-        return  encodedRectangle.contains(scratchTriangle.aX, scratchTriangle.aY) 
-                ? Component2D.WithinRelation.NOTWITHIN : Component2D.WithinRelation.DISJOINT;
-      }
-      case LINE: {
-        return encodedRectangle.withinLine(scratchTriangle.aX, scratchTriangle.aY, scratchTriangle.ab,
-                scratchTriangle.bX, scratchTriangle.bY);
-      }
-      case TRIANGLE: {
-        return encodedRectangle.withinTriangle(scratchTriangle.aX, scratchTriangle.aY, scratchTriangle.ab,
-                scratchTriangle.bX, scratchTriangle.bY, scratchTriangle.bc,
-                scratchTriangle.cX, scratchTriangle.cY, scratchTriangle.ca);
-      }
-      default: throw new IllegalArgumentException("Unsupported triangle type :[" + scratchTriangle.type + "]");
-    }
+  protected SpatialVisitor getSpatialVisitor() {
+    final EncodedRectangle encodedRectangle =
+            new EncodedRectangle(
+                    rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon);
+    return new SpatialVisitor() {
+
+      @Override
+      protected Relation relate(byte[] minTriangle, byte[] maxTriangle) {
+        if (queryRelation == QueryRelation.INTERSECTS || queryRelation == QueryRelation.DISJOINT) {
+          return encodedRectangle.intersectRangeBBox(
+                  ShapeField.BYTES,
+                  0,
+                  minTriangle,
+                  3 * ShapeField.BYTES,
+                  2 * ShapeField.BYTES,
+                  maxTriangle);
+        }
+        return encodedRectangle.relateRangeBBox(
+                ShapeField.BYTES,
+                0,
+                minTriangle,
+                3 * ShapeField.BYTES,
+                2 * ShapeField.BYTES,
+                maxTriangle);
+      }
+
+      @Override
+      protected Predicate<byte[]> intersects() {
+        final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
+        return triangle -> {
+          ShapeField.decodeTriangle(triangle, scratchTriangle);
+
+          switch (scratchTriangle.type) {
+            case POINT:
+            {
+              return encodedRectangle.contains(scratchTriangle.aX, scratchTriangle.aY);
+            }
+            case LINE:
+            {
+              int aY = scratchTriangle.aY;
+              int aX = scratchTriangle.aX;
+              int bY = scratchTriangle.bY;
+              int bX = scratchTriangle.bX;
+              return encodedRectangle.intersectsLine(aX, aY, bX, bY);
+            }
+            case TRIANGLE:
+            {
+              int aY = scratchTriangle.aY;
+              int aX = scratchTriangle.aX;
+              int bY = scratchTriangle.bY;
+              int bX = scratchTriangle.bX;
+              int cY = scratchTriangle.cY;
+              int cX = scratchTriangle.cX;
+              return encodedRectangle.intersectsTriangle(aX, aY, bX, bY, cX, cY);
+            }
+            default:
+              throw new IllegalArgumentException(
+                      "Unsupported triangle type :[" + scratchTriangle.type + "]");
+          }
+        };
+      }
+
+      @Override
+      protected Predicate<byte[]> within() {
+        final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
+        return triangle -> {
+          ShapeField.decodeTriangle(triangle, scratchTriangle);
+
+          switch (scratchTriangle.type) {
+            case POINT:
+            {
+              return encodedRectangle.contains(scratchTriangle.aX, scratchTriangle.aY);
+            }
+            case LINE:
+            {
+              int aY = scratchTriangle.aY;
+              int aX = scratchTriangle.aX;
+              int bY = scratchTriangle.bY;
+              int bX = scratchTriangle.bX;
+              return encodedRectangle.containsLine(aX, aY, bX, bY);
+            }
+            case TRIANGLE:
+            {
+              int aY = scratchTriangle.aY;
+              int aX = scratchTriangle.aX;
+              int bY = scratchTriangle.bY;
+              int bX = scratchTriangle.bX;
+              int cY = scratchTriangle.cY;
+              int cX = scratchTriangle.cX;
+              return encodedRectangle.containsTriangle(aX, aY, bX, bY, cX, cY);
+            }
+            default:
+              throw new IllegalArgumentException(
+                      "Unsupported triangle type :[" + scratchTriangle.type + "]");
+          }
+        };
+      }
+
+      @Override
+      protected Function<byte[], Component2D.WithinRelation> contains() {
+        if (encodedRectangle.crossesDateline()) {
+          throw new IllegalArgumentException(
+                  "withinTriangle is not supported for rectangles crossing the date line");
+        }
+        final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
+        return triangle -> {
+
+          // decode indexed triangle
+          ShapeField.decodeTriangle(triangle, scratchTriangle);
+
+          switch (scratchTriangle.type) {
+            case POINT:
+            {
+              return encodedRectangle.contains(scratchTriangle.aX, scratchTriangle.aY)
+                      ? Component2D.WithinRelation.NOTWITHIN
+                      : Component2D.WithinRelation.DISJOINT;
+            }
+            case LINE:
+            {
+              return encodedRectangle.withinLine(
+                      scratchTriangle.aX,
+                      scratchTriangle.aY,
+                      scratchTriangle.ab,
+                      scratchTriangle.bX,
+                      scratchTriangle.bY);
+            }
+            case TRIANGLE:
+            {
+              return encodedRectangle.withinTriangle(
+                      scratchTriangle.aX,
+                      scratchTriangle.aY,
+                      scratchTriangle.ab,
+                      scratchTriangle.bX,
+                      scratchTriangle.bY,
+                      scratchTriangle.bc,
+                      scratchTriangle.cX,
+                      scratchTriangle.cY,
+                      scratchTriangle.ca);
+            }
+            default:
+              throw new IllegalArgumentException(
+                      "Unsupported triangle type :[" + scratchTriangle.type + "]");
+          }
+        };
+      }
+    };
   }
 
   @Override
   protected boolean equalsTo(Object o) {
-    return super.equalsTo(o) && rectangle.equals(((LatLonShapeBoundingBoxQuery)o).rectangle);
+    return super.equalsTo(o) && rectangle.equals(((LatLonShapeBoundingBoxQuery) o).rectangle);
   }
 
   @Override
@@ -183,7 +240,7 @@ final class LatLonShapeBoundingBoxQuery extends ShapeQuery {
       }
       this.minX = encodeLongitudeCeil(minLon);
       this.maxX = encodeLongitude(maxLon);
-      this.minY  = encodeLatitudeCeil(minLat);
+      this.minY = encodeLatitudeCeil(minLat);
       this.maxY = encodeLatitude(maxLat);
       this.crossesDateline = minLon > maxLon;
       if (this.crossesDateline) {
@@ -197,10 +254,9 @@ final class LatLonShapeBoundingBoxQuery extends ShapeQuery {
       }
     }
 
-    /**
-     * encodes a bounding box into the provided byte array
-     */
-    private static void encode(final int minX, final int maxX, final int minY, final int maxY, byte[] b) {
+    /** encodes a bounding box into the provided byte array */
+    private static void encode(
+            final int minX, final int maxX, final int minY, final int maxY, byte[] b) {
       if (b == null) {
         b = new byte[4 * BYTES];
       }
@@ -214,47 +270,71 @@ final class LatLonShapeBoundingBoxQuery extends ShapeQuery {
       return crossesDateline;
     }
 
-    /**
-     * compare this to a provided range bounding box
-     **/
-    Relation relateRangeBBox(int minXOffset, int minYOffset, byte[] minTriangle,
-                             int maxXOffset, int maxYOffset, byte[] maxTriangle) {
-      Relation eastRelation = compareBBoxToRangeBBox(this.bbox,
-              minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle);
+    /** compare this to a provided range bounding box */
+    Relation relateRangeBBox(
+            int minXOffset,
+            int minYOffset,
+            byte[] minTriangle,
+            int maxXOffset,
+            int maxYOffset,
+            byte[] maxTriangle) {
+      Relation eastRelation =
+              compareBBoxToRangeBBox(
+                      this.bbox, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle);
       if (this.crossesDateline() && eastRelation == Relation.CELL_OUTSIDE_QUERY) {
-        return compareBBoxToRangeBBox(this.west, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle);
+        return compareBBoxToRangeBBox(
+                this.west, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle);
       }
       return eastRelation;
     }
 
-    /**
-     * intersects this to a provided range bounding box
-     **/
-    Relation intersectRangeBBox(int minXOffset, int minYOffset, byte[] minTriangle,
-                                int maxXOffset, int maxYOffset, byte[] maxTriangle) {
-      Relation eastRelation = intersectBBoxWithRangeBBox(this.bbox,
-              minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle);
+    /** intersects this to a provided range bounding box */
+    Relation intersectRangeBBox(
+            int minXOffset,
+            int minYOffset,
+            byte[] minTriangle,
+            int maxXOffset,
+            int maxYOffset,
+            byte[] maxTriangle) {
+      Relation eastRelation =
+              intersectBBoxWithRangeBBox(
+                      this.bbox, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle);
       if (this.crossesDateline() && eastRelation == Relation.CELL_OUTSIDE_QUERY) {
-        return intersectBBoxWithRangeBBox(this.west, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle);
+        return intersectBBoxWithRangeBBox(
+                this.west, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle);
       }
       return eastRelation;
     }
 
     /**
-     * static utility method to compare a bbox with a range of triangles (just the bbox of the triangle collection)
-     **/
-    private static Relation compareBBoxToRangeBBox(final byte[] bbox,
-                                                   int minXOffset, int minYOffset, byte[] minTriangle,
-                                                   int maxXOffset, int maxYOffset, byte[] maxTriangle) {
+     * static utility method to compare a bbox with a range of triangles (just the bbox of the
+     * triangle collection)
+     */
+    private static Relation compareBBoxToRangeBBox(
+            final byte[] bbox,
+            int minXOffset,
+            int minYOffset,
+            byte[] minTriangle,
+            int maxXOffset,
+            int maxYOffset,
+            byte[] maxTriangle) {
       // check bounding box (DISJOINT)
-      if (disjoint(bbox, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle)) {
+      if (disjoint(
+              bbox, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle)) {
         return Relation.CELL_OUTSIDE_QUERY;
       }
 
-      if (FutureArrays.compareUnsigned(minTriangle, minXOffset, minXOffset + BYTES, bbox, BYTES, 2 * BYTES) >= 0 &&
-          FutureArrays.compareUnsigned(maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES) <= 0 &&
-          FutureArrays.compareUnsigned(minTriangle, minYOffset, minYOffset + BYTES, bbox, 0, BYTES) >= 0 &&
-          FutureArrays.compareUnsigned(maxTriangle, maxYOffset, maxYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES) <= 0) {
+      if (FutureArrays.compareUnsigned(
+              minTriangle, minXOffset, minXOffset + BYTES, bbox, BYTES, 2 * BYTES)
+              >= 0
+              && FutureArrays.compareUnsigned(
+              maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES)
+              <= 0
+              && FutureArrays.compareUnsigned(minTriangle, minYOffset, minYOffset + BYTES, bbox, 0, BYTES)
+              >= 0
+              && FutureArrays.compareUnsigned(
+              maxTriangle, maxYOffset, maxYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES)
+              <= 0) {
         return Relation.CELL_INSIDE_QUERY;
       }
 
@@ -262,37 +342,64 @@ final class LatLonShapeBoundingBoxQuery extends ShapeQuery {
     }
 
     /**
-     * static utility method to compare a bbox with a range of triangles (just the bbox of the triangle collection)
-     * for intersection
-     **/
-    private static Relation intersectBBoxWithRangeBBox(final byte[] bbox,
-                                                       int minXOffset, int minYOffset, byte[] minTriangle,
-                                                       int maxXOffset, int maxYOffset, byte[] maxTriangle) {
+     * static utility method to compare a bbox with a range of triangles (just the bbox of the
+     * triangle collection) for intersection
+     */
+    private static Relation intersectBBoxWithRangeBBox(
+            final byte[] bbox,
+            int minXOffset,
+            int minYOffset,
+            byte[] minTriangle,
+            int maxXOffset,
+            int maxYOffset,
+            byte[] maxTriangle) {
       // check bounding box (DISJOINT)
-      if (disjoint(bbox, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle)) {
+      if (disjoint(
+              bbox, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle)) {
         return Relation.CELL_OUTSIDE_QUERY;
       }
 
-      if (FutureArrays.compareUnsigned(minTriangle, minXOffset, minXOffset + BYTES, bbox, BYTES, 2 * BYTES) >= 0 &&
-          FutureArrays.compareUnsigned(minTriangle, minYOffset, minYOffset + BYTES, bbox, 0, BYTES) >= 0) {
-        if (FutureArrays.compareUnsigned(maxTriangle, minXOffset, minXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES) <= 0 &&
-            FutureArrays.compareUnsigned(maxTriangle, maxYOffset, maxYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES) <= 0) {
+      if (FutureArrays.compareUnsigned(
+              minTriangle, minXOffset, minXOffset + BYTES, bbox, BYTES, 2 * BYTES)
+              >= 0
+              && FutureArrays.compareUnsigned(minTriangle, minYOffset, minYOffset + BYTES, bbox, 0, BYTES)
+              >= 0) {
+        if (FutureArrays.compareUnsigned(
+                maxTriangle, minXOffset, minXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES)
+                <= 0
+                && FutureArrays.compareUnsigned(
+                maxTriangle, maxYOffset, maxYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES)
+                <= 0) {
           return Relation.CELL_INSIDE_QUERY;
         }
-        if (FutureArrays.compareUnsigned(maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES) <= 0 &&
-                FutureArrays.compareUnsigned(maxTriangle, minYOffset, minYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES) <= 0) {
+        if (FutureArrays.compareUnsigned(
+                maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES)
+                <= 0
+                && FutureArrays.compareUnsigned(
+                maxTriangle, minYOffset, minYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES)
+                <= 0) {
           return Relation.CELL_INSIDE_QUERY;
         }
       }
 
-      if (FutureArrays.compareUnsigned(maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES) <= 0 &&
-          FutureArrays.compareUnsigned(maxTriangle, maxYOffset, maxYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES) <= 0) {
-        if (FutureArrays.compareUnsigned(minTriangle, minXOffset, minXOffset + BYTES, bbox, BYTES, 2 * BYTES) >= 0 &&
-            FutureArrays.compareUnsigned(minTriangle, maxYOffset, maxYOffset + BYTES, bbox, 0, BYTES) >= 0) {
+      if (FutureArrays.compareUnsigned(
+              maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES)
+              <= 0
+              && FutureArrays.compareUnsigned(
+              maxTriangle, maxYOffset, maxYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES)
+              <= 0) {
+        if (FutureArrays.compareUnsigned(
+                minTriangle, minXOffset, minXOffset + BYTES, bbox, BYTES, 2 * BYTES)
+                >= 0
+                && FutureArrays.compareUnsigned(minTriangle, maxYOffset, maxYOffset + BYTES, bbox, 0, BYTES)
+                >= 0) {
           return Relation.CELL_INSIDE_QUERY;
         }
-        if (FutureArrays.compareUnsigned(minTriangle, maxXOffset, maxXOffset + BYTES, bbox, BYTES, 2 * BYTES) >= 0 &&
-                FutureArrays.compareUnsigned(minTriangle, minYOffset, minYOffset + BYTES, bbox, 0, BYTES) >= 0) {
+        if (FutureArrays.compareUnsigned(
+                minTriangle, maxXOffset, maxXOffset + BYTES, bbox, BYTES, 2 * BYTES)
+                >= 0
+                && FutureArrays.compareUnsigned(minTriangle, minYOffset, minYOffset + BYTES, bbox, 0, BYTES)
+                >= 0) {
           return Relation.CELL_INSIDE_QUERY;
         }
       }
@@ -300,21 +407,29 @@ final class LatLonShapeBoundingBoxQuery extends ShapeQuery {
       return Relation.CELL_CROSSES_QUERY;
     }
 
-    /**
-     * static utility method to check a bbox is disjoint with a range of triangles
-     **/
-    private static boolean disjoint(final byte[] bbox,
-                                    int minXOffset, int minYOffset, byte[] minTriangle,
-                                    int maxXOffset, int maxYOffset, byte[] maxTriangle) {
-      return FutureArrays.compareUnsigned(minTriangle, minXOffset, minXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES) > 0 ||
-             FutureArrays.compareUnsigned(maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, BYTES, 2 * BYTES) < 0 ||
-             FutureArrays.compareUnsigned(minTriangle, minYOffset, minYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES) > 0 ||
-             FutureArrays.compareUnsigned(maxTriangle, maxYOffset, maxYOffset + BYTES, bbox, 0, BYTES) < 0;
+    /** static utility method to check a bbox is disjoint with a range of triangles */
+    private static boolean disjoint(
+            final byte[] bbox,
+            int minXOffset,
+            int minYOffset,
+            byte[] minTriangle,
+            int maxXOffset,
+            int maxYOffset,
+            byte[] maxTriangle) {
+      return FutureArrays.compareUnsigned(
+              minTriangle, minXOffset, minXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES)
+              > 0
+              || FutureArrays.compareUnsigned(
+              maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, BYTES, 2 * BYTES)
+              < 0
+              || FutureArrays.compareUnsigned(
+              minTriangle, minYOffset, minYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES)
+              > 0
+              || FutureArrays.compareUnsigned(maxTriangle, maxYOffset, maxYOffset + BYTES, bbox, 0, BYTES)
+              < 0;
     }
 
-    /**
-     * Checks if the rectangle contains the provided point
-     **/
+    /** Checks if the rectangle contains the provided point */
     boolean contains(int x, int y) {
       if (y < minY || y > maxY) {
         return false;
@@ -326,9 +441,7 @@ final class LatLonShapeBoundingBoxQuery extends ShapeQuery {
       }
     }
 
-    /**
-     * Checks if the rectangle intersects the provided LINE
-     **/
+    /** Checks if the rectangle intersects the provided LINE */
     boolean intersectsLine(int aX, int aY, int bX, int bY) {
       if (contains(aX, aY) || contains(bX, bY)) {
         return true;
@@ -350,9 +463,7 @@ final class LatLonShapeBoundingBoxQuery extends ShapeQuery {
       return edgeIntersectsQuery(aX, aY, bX, bY);
     }
 
-    /**
-     * Checks if the rectangle intersects the provided triangle
-     **/
+    /** Checks if the rectangle intersects the provided triangle */
     boolean intersectsTriangle(int aX, int aY, int bX, int bY, int cX, int cY) {
       // query contains any triangle points
       if (contains(aX, aY) || contains(bX, bY) || contains(cX, cY)) {
@@ -377,49 +488,38 @@ final class LatLonShapeBoundingBoxQuery extends ShapeQuery {
         }
       }
       // expensive part
-      return Component2D.pointInTriangle(tMinX, tMaxX, tMinY, tMaxY, minX, minY, aX, aY, bX, bY, cX, cY) ||
-              edgeIntersectsQuery(aX, aY, bX, bY) ||
-              edgeIntersectsQuery(bX, bY, cX, cY) ||
-              edgeIntersectsQuery(cX, cY, aX, aY);
+      return Component2D.pointInTriangle(
+              tMinX, tMaxX, tMinY, tMaxY, minX, minY, aX, aY, bX, bY, cX, cY)
+              || edgeIntersectsQuery(aX, aY, bX, bY)
+              || edgeIntersectsQuery(bX, bY, cX, cY)
+              || edgeIntersectsQuery(cX, cY, aX, aY);
     }
 
-    /**
-     * Checks if the rectangle contains the provided LINE
-     **/
+    /** Checks if the rectangle contains the provided LINE */
     boolean containsLine(int aX, int aY, int bX, int bY) {
-      if (aY < minY || bY < minY ||
-              aY > maxY || bY > maxY ) {
+      if (aY < minY || bY < minY || aY > maxY || bY > maxY) {
         return false;
       }
       if (crossesDateline) { // crosses dateline
-        return (aX >= minX && bX >= minX) ||
-                (aX <= maxX && bX <= maxX);
+        return (aX >= minX && bX >= minX) || (aX <= maxX && bX <= maxX);
       } else {
-        return aX >= minX && bX >= minX &&
-                aX <= maxX && bX <= maxX;
+        return aX >= minX && bX >= minX && aX <= maxX && bX <= maxX;
       }
     }
 
-    /**
-     * Checks if the rectangle contains the provided triangle
-     **/
+    /** Checks if the rectangle contains the provided triangle */
     boolean containsTriangle(int aX, int aY, int bX, int bY, int cX, int cY) {
-      if (aY < minY || bY < minY || cY < minY ||
-              aY > maxY || bY > maxY || cY > maxY) {
+      if (aY < minY || bY < minY || cY < minY || aY > maxY || bY > maxY || cY > maxY) {
         return false;
       }
       if (crossesDateline) { // crosses dateline
-        return (aX >= minX && bX >= minX && cX >= minX) ||
-                (aX <= maxX && bX <= maxX && cX <= maxX);
+        return (aX >= minX && bX >= minX && cX >= minX) || (aX <= maxX && bX <= maxX && cX <= maxX);
       } else {
-        return aX >= minX && bX >= minX && cX >= minX &&
-                aX <= maxX && bX <= maxX && cX <= maxX;
+        return aX >= minX && bX >= minX && cX >= minX && aX <= maxX && bX <= maxX && cX <= maxX;
       }
     }
 
-    /**
-     * Returns the Within relation to the provided triangle
-     */
+    /** Returns the Within relation to the provided triangle */
     Component2D.WithinRelation withinLine(int ax, int ay, boolean ab, int bx, int by) {
       if (ab == true && edgeIntersectsBox(ax, ay, bx, by, minX, maxX, minY, maxY) == true) {
         return Component2D.WithinRelation.NOTWITHIN;
@@ -427,10 +527,9 @@ final class LatLonShapeBoundingBoxQuery extends ShapeQuery {
       return Component2D.WithinRelation.DISJOINT;
     }
 
-    /**
-     * Returns the Within relation to the provided triangle
-     */
-    Component2D.WithinRelation withinTriangle(int aX, int aY, boolean ab, int bX, int bY, boolean bc, int cX, int cY, boolean ca) {
+    /** Returns the Within relation to the provided triangle */
+    Component2D.WithinRelation withinTriangle(
+            int aX, int aY, boolean ab, int bX, int bY, boolean bc, int cX, int cY, boolean ca) {
       // Points belong to the shape so if points are inside the rectangle then it cannot be within.
       if (contains(aX, aY) || contains(bX, bY) || contains(cX, cY)) {
         return Component2D.WithinRelation.NOTWITHIN;
@@ -479,16 +578,15 @@ final class LatLonShapeBoundingBoxQuery extends ShapeQuery {
         }
       }
       // Check if shape is within the triangle
-      if (relation == Component2D.WithinRelation.CANDIDATE ||
-              Component2D.pointInTriangle(tMinX, tMaxX, tMinY, tMaxY, minX, minY, aX, aY, bX, bY, cX, cY)) {
+      if (relation == Component2D.WithinRelation.CANDIDATE
+              || Component2D.pointInTriangle(
+              tMinX, tMaxX, tMinY, tMaxY, minX, minY, aX, aY, bX, bY, cX, cY)) {
         return Component2D.WithinRelation.CANDIDATE;
       }
       return relation;
     }
 
-    /**
-     * returns true if the edge (defined by (aX, aY) (bX, bY)) intersects the query
-     */
+    /** returns true if the edge (defined by (aX, aY) (bX, bY)) intersects the query */
     private boolean edgeIntersectsQuery(int aX, int aY, int bX, int bY) {
       if (crossesDateline) {
         return edgeIntersectsBox(aX, aY, bX, bY, MIN_LON_ENCODED, this.maxX, this.minY, this.maxY)
@@ -497,18 +595,22 @@ final class LatLonShapeBoundingBoxQuery extends ShapeQuery {
       return edgeIntersectsBox(aX, aY, bX, bY, this.minX, this.maxX, this.minY, this.maxY);
     }
 
-    /**
-     * returns true if the edge (defined by (aX, aY) (bX, bY)) intersects the box
-     */
-    private static boolean edgeIntersectsBox(int aX, int aY, int bX, int bY,
-                                             int minX, int maxX, int minY, int maxY) {
-      if (Math.max(aX, bX) < minX || Math.min(aX, bX) > maxX || Math.min(aY, bY) > maxY || Math.max(aY, bY) < minY) {
+    /** returns true if the edge (defined by (aX, aY) (bX, bY)) intersects the box */
+    private static boolean edgeIntersectsBox(
+            int aX, int aY, int bX, int bY, int minX, int maxX, int minY, int maxY) {
+      if (Math.max(aX, bX) < minX
+              || Math.min(aX, bX) > maxX
+              || Math.min(aY, bY) > maxY
+              || Math.max(aY, bY) < minY) {
         return false;
       }
-      return GeoUtils.lineCrossesLineWithBoundary(aX, aY, bX, bY, minX, maxY,  maxX, maxY) || // top
-              GeoUtils.lineCrossesLineWithBoundary(aX, aY, bX, bY, maxX, maxY,  maxX, minY) || // bottom
-              GeoUtils.lineCrossesLineWithBoundary(aX, aY, bX, bY, maxX, minY,  minX, minY) || // left
-              GeoUtils.lineCrossesLineWithBoundary(aX, aY, bX, bY, minX, minY,  minX, maxY);   // right
+      return GeoUtils.lineCrossesLineWithBoundary(aX, aY, bX, bY, minX, maxY, maxX, maxY)
+              || // top
+              GeoUtils.lineCrossesLineWithBoundary(aX, aY, bX, bY, maxX, maxY, maxX, minY)
+              || // bottom
+              GeoUtils.lineCrossesLineWithBoundary(aX, aY, bX, bY, maxX, minY, minX, minY)
+              || // left
+              GeoUtils.lineCrossesLineWithBoundary(aX, aY, bX, bY, minX, minY, minX, maxY); // right
     }
   }
 }
diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonShapeQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonShapeQuery.java
index 400a6f2..beed2a6 100644
--- a/lucene/core/src/java/org/apache/lucene/document/LatLonShapeQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/LatLonShapeQuery.java
@@ -17,7 +17,8 @@
 package org.apache.lucene.document;
 
 import java.util.Arrays;
-
+import java.util.function.Function;
+import java.util.function.Predicate;
 import org.apache.lucene.document.ShapeField.QueryRelation;
 import org.apache.lucene.geo.Component2D;
 import org.apache.lucene.geo.GeoEncodingUtils;
@@ -27,15 +28,14 @@ import org.apache.lucene.index.PointValues.Relation;
 import org.apache.lucene.util.NumericUtils;
 
 /**
- * Finds all previously indexed cartesian shapes that comply the given {@link QueryRelation} with
- * the specified array of {@link LatLonGeometry}.
+ * Finds all previously indexed geo shapes that comply the given {@link QueryRelation} with the
+ * specified array of {@link LatLonGeometry}.
  *
  * <p>The field must be indexed using {@link LatLonShape#createIndexableFields} added per document.
- *
- **/
-final class LatLonShapeQuery extends ShapeQuery {
-  final private LatLonGeometry[] geometries;
-  final private Component2D component2D;
+ */
+final class LatLonShapeQuery extends SpatialQuery {
+  private final LatLonGeometry[] geometries;
+  private final Component2D component2D;
 
   /**
    * Creates a query that matches all indexed shapes to the provided array of {@link LatLonGeometry}
@@ -46,116 +46,162 @@ final class LatLonShapeQuery extends ShapeQuery {
       for (LatLonGeometry geometry : geometries) {
         if (geometry instanceof Line) {
           // TODO: line queries do not support within relations
-          throw new IllegalArgumentException("LatLonShapeQuery does not support " + QueryRelation.WITHIN + " queries with line geometries");
+          throw new IllegalArgumentException(
+                  "LatLonShapeQuery does not support "
+                          + QueryRelation.WITHIN
+                          + " queries with line geometries");
         }
       }
-
     }
     this.component2D = LatLonGeometry.create(geometries);
     this.geometries = geometries.clone();
   }
 
   @Override
-  protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle,
-                                            int maxXOffset, int maxYOffset, byte[] maxTriangle) {
-
-    double minLat = GeoEncodingUtils.decodeLatitude(NumericUtils.sortableBytesToInt(minTriangle, minYOffset));
-    double minLon = GeoEncodingUtils.decodeLongitude(NumericUtils.sortableBytesToInt(minTriangle, minXOffset));
-    double maxLat = GeoEncodingUtils.decodeLatitude(NumericUtils.sortableBytesToInt(maxTriangle, maxYOffset));
-    double maxLon = GeoEncodingUtils.decodeLongitude(NumericUtils.sortableBytesToInt(maxTriangle, maxXOffset));
-
-    // check internal node against query
-    return component2D.relate(minLon, maxLon, minLat, maxLat);
-  }
-
-  @Override
-  protected boolean queryIntersects(byte[] t, ShapeField.DecodedTriangle scratchTriangle) {
-    ShapeField.decodeTriangle(t, scratchTriangle);
-
-    switch (scratchTriangle.type) {
-      case POINT: {
-        double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
-        double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
-        return component2D.contains(alon, alat);
+  protected SpatialVisitor getSpatialVisitor() {
+
+    return new SpatialVisitor() {
+      @Override
+      protected Relation relate(byte[] minTriangle, byte[] maxTriangle) {
+        double minLat =
+                GeoEncodingUtils.decodeLatitude(NumericUtils.sortableBytesToInt(minTriangle, 0));
+        double minLon =
+                GeoEncodingUtils.decodeLongitude(
+                        NumericUtils.sortableBytesToInt(minTriangle, ShapeField.BYTES));
+        double maxLat =
+                GeoEncodingUtils.decodeLatitude(
+                        NumericUtils.sortableBytesToInt(maxTriangle, 2 * ShapeField.BYTES));
+        double maxLon =
+                GeoEncodingUtils.decodeLongitude(
+                        NumericUtils.sortableBytesToInt(maxTriangle, 3 * ShapeField.BYTES));
+
+        // check internal node against query
+        return component2D.relate(minLon, maxLon, minLat, maxLat);
       }
-      case LINE: {
-        double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
-        double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
-        double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY);
-        double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX);
-        return component2D.intersectsLine(alon, alat, blon, blat);
-      }
-      case TRIANGLE: {
-        double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
-        double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
-        double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY);
-        double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX);
-        double clat = GeoEncodingUtils.decodeLatitude(scratchTriangle.cY);
-        double clon = GeoEncodingUtils.decodeLongitude(scratchTriangle.cX);
-        return component2D.intersectsTriangle(alon, alat, blon, blat, clon, clat);
-      }
-      default: throw new IllegalArgumentException("Unsupported triangle type :[" + scratchTriangle.type + "]");
-    }
-  }
-
-  @Override
-  protected boolean queryContains(byte[] t, ShapeField.DecodedTriangle scratchTriangle) {
-    ShapeField.decodeTriangle(t, scratchTriangle);
 
-    switch (scratchTriangle.type) {
-      case POINT: {
-        double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
-        double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
-        return component2D.contains(alon, alat);
+      @Override
+      protected Predicate<byte[]> intersects() {
+        final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
+        return triangle -> {
+          ShapeField.decodeTriangle(triangle, scratchTriangle);
+
+          switch (scratchTriangle.type) {
+            case POINT:
+            {
+              double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
+              double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
+              return component2D.contains(alon, alat);
+            }
+            case LINE:
+            {
+              double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
+              double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
+              double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY);
+              double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX);
+              return component2D.intersectsLine(alon, alat, blon, blat);
+            }
+            case TRIANGLE:
+            {
+              double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
+              double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
+              double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY);
+              double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX);
+              double clat = GeoEncodingUtils.decodeLatitude(scratchTriangle.cY);
+              double clon = GeoEncodingUtils.decodeLongitude(scratchTriangle.cX);
+              return component2D.intersectsTriangle(alon, alat, blon, blat, clon, clat);
+            }
+            default:
+              throw new IllegalArgumentException(
+                      "Unsupported triangle type :[" + scratchTriangle.type + "]");
+          }
+        };
       }
-      case LINE: {
-        double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
-        double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
-        double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY);
-        double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX);
-        return component2D.containsLine(alon, alat, blon, blat);
-      }
-      case TRIANGLE: {
-        double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
-        double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
-        double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY);
-        double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX);
-        double clat = GeoEncodingUtils.decodeLatitude(scratchTriangle.cY);
-        double clon = GeoEncodingUtils.decodeLongitude(scratchTriangle.cX);
-        return component2D.containsTriangle(alon, alat, blon, blat, clon, clat);
-      }
-      default: throw new IllegalArgumentException("Unsupported triangle type :[" + scratchTriangle.type + "]");
-    }
-  }
-
-  @Override
-  protected Component2D.WithinRelation queryWithin(byte[] t, ShapeField.DecodedTriangle scratchTriangle) {
-    ShapeField.decodeTriangle(t, scratchTriangle);
 
-    switch (scratchTriangle.type) {
-      case POINT: {
-        double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
-        double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
-        return component2D.withinPoint(alon, alat);
+      @Override
+      protected Predicate<byte[]> within() {
+        final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
+        return triangle -> {
+          ShapeField.decodeTriangle(triangle, scratchTriangle);
+
+          switch (scratchTriangle.type) {
+            case POINT:
+            {
+              double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
+              double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
+              return component2D.contains(alon, alat);
+            }
+            case LINE:
+            {
+              double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
+              double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
+              double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY);
+              double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX);
+              return component2D.containsLine(alon, alat, blon, blat);
+            }
+            case TRIANGLE:
+            {
+              double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
+              double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
+              double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY);
+              double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX);
+              double clat = GeoEncodingUtils.decodeLatitude(scratchTriangle.cY);
+              double clon = GeoEncodingUtils.decodeLongitude(scratchTriangle.cX);
+              return component2D.containsTriangle(alon, alat, blon, blat, clon, clat);
+            }
+            default:
+              throw new IllegalArgumentException(
+                      "Unsupported triangle type :[" + scratchTriangle.type + "]");
+          }
+        };
       }
-      case LINE: {
-        double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
-        double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
-        double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY);
-        double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX);
-        return component2D.withinLine(alon, alat, scratchTriangle.ab, blon, blat);
-      }
-      case TRIANGLE: {
-        double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
-        double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
-        double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY);
-        double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX);
-        double clat = GeoEncodingUtils.decodeLatitude(scratchTriangle.cY);
-        double clon = GeoEncodingUtils.decodeLongitude(scratchTriangle.cX);
-        return component2D.withinTriangle(alon, alat, scratchTriangle.ab, blon, blat, scratchTriangle.bc, clon, clat, scratchTriangle.ca);
+
+      @Override
+      protected Function<byte[], Component2D.WithinRelation> contains() {
+        final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
+        return triangle -> {
+          ShapeField.decodeTriangle(triangle, scratchTriangle);
+
+          switch (scratchTriangle.type) {
+            case POINT:
+            {
+              double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
+              double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
+              return component2D.withinPoint(alon, alat);
+            }
+            case LINE:
+            {
+              double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
+              double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
+              double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY);
+              double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX);
+              return component2D.withinLine(alon, alat, scratchTriangle.ab, blon, blat);
+            }
+            case TRIANGLE:
+            {
+              double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY);
+              double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX);
+              double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY);
+              double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX);
+              double clat = GeoEncodingUtils.decodeLatitude(scratchTriangle.cY);
+              double clon = GeoEncodingUtils.decodeLongitude(scratchTriangle.cX);
+              return component2D.withinTriangle(
+                      alon,
+                      alat,
+                      scratchTriangle.ab,
+                      blon,
+                      blat,
+                      scratchTriangle.bc,
+                      clon,
+                      clat,
+                      scratchTriangle.ca);
+            }
+            default:
+              throw new IllegalArgumentException(
+                      "Unsupported triangle type :[" + scratchTriangle.type + "]");
+          }
+        };
       }
-      default: throw new IllegalArgumentException("Unsupported triangle type :[" + scratchTriangle.type + "]");
-    }
+    };
   }
 
   @Override
@@ -179,7 +225,7 @@ final class LatLonShapeQuery extends ShapeQuery {
 
   @Override
   protected boolean equalsTo(Object o) {
-    return super.equalsTo(o) && Arrays.equals(geometries, ((LatLonShapeQuery)o).geometries);
+    return super.equalsTo(o) && Arrays.equals(geometries, ((LatLonShapeQuery) o).geometries);
   }
 
   @Override
@@ -188,4 +234,4 @@ final class LatLonShapeQuery extends ShapeQuery {
     hash = 31 * hash + Arrays.hashCode(geometries);
     return hash;
   }
-}
\ No newline at end of file
+}
diff --git a/lucene/core/src/java/org/apache/lucene/document/ShapeQuery.java b/lucene/core/src/java/org/apache/lucene/document/ShapeQuery.java
deleted file mode 100644
index e8ea282..0000000
--- a/lucene/core/src/java/org/apache/lucene/document/ShapeQuery.java
+++ /dev/null
@@ -1,621 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.lucene.document;
-
-import java.io.IOException;
-import java.util.Objects;
-
-import org.apache.lucene.document.ShapeField.QueryRelation;
-import org.apache.lucene.geo.Component2D;
-import org.apache.lucene.index.FieldInfo;
-import org.apache.lucene.index.LeafReader;
-import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.PointValues;
-import org.apache.lucene.index.PointValues.IntersectVisitor;
-import org.apache.lucene.index.PointValues.Relation;
-import org.apache.lucene.search.CollectionTerminatedException;
-import org.apache.lucene.search.ConstantScoreScorer;
-import org.apache.lucene.search.ConstantScoreWeight;
-import org.apache.lucene.search.DocIdSetIterator;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.QueryVisitor;
-import org.apache.lucene.search.ScoreMode;
-import org.apache.lucene.search.Scorer;
-import org.apache.lucene.search.ScorerSupplier;
-import org.apache.lucene.search.Weight;
-import org.apache.lucene.util.BitSetIterator;
-import org.apache.lucene.util.DocIdSetBuilder;
-import org.apache.lucene.util.FixedBitSet;
-
-/**
- * Base query class for all spatial geometries: {@link LatLonShape} and {@link XYShape}.
- *
- * <p>The field must be indexed using either {@link LatLonShape#createIndexableFields} or
- * {@link XYShape#createIndexableFields} and the corresponding factory method must be used:
- * <ul>
- *   <li>{@link LatLonShape#newBoxQuery newBoxQuery()} for matching geo shapes that have some {@link QueryRelation} with a bounding box.
- *   <li>{@link LatLonShape#newLineQuery newLineQuery()} for matching geo shapes that have some {@link QueryRelation} with a linestring.
- *   <li>{@link LatLonShape#newPolygonQuery newPolygonQuery()} for matching geo shapes that have some {@link QueryRelation} with a polygon.
- *   <li>{@link XYShape#newBoxQuery newBoxQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a bounding box.
- *   <li>{@link XYShape#newLineQuery newLineQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a linestring.
- *   <li>{@link XYShape#newPolygonQuery newPolygonQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a polygon.
- * </ul>
- **/
-abstract class ShapeQuery extends Query {
-  /** field name */
-  final String field;
-  /** query relation
-   * disjoint: {@link QueryRelation#DISJOINT},
-   * intersects: {@link QueryRelation#INTERSECTS},
-   * within: {@link QueryRelation#DISJOINT},
-   * contains: {@link QueryRelation#CONTAINS}
-   * */
-  final QueryRelation queryRelation;
-
-  protected ShapeQuery(String field, final QueryRelation queryType) {
-    if (field == null) {
-      throw new IllegalArgumentException("field must not be null");
-    }
-    this.field = field;
-    this.queryRelation = queryType;
-  }
-
-  /**
-   *   relates an internal node (bounding box of a range of triangles) to the target query
-   *   Note: logic is specific to query type
-   *   see {@link LatLonShapeBoundingBoxQuery#relateRangeToQuery} and {@link LatLonShapeQuery#relateRangeToQuery}
-   */
-  protected abstract Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle,
-                                                     int maxXOffset, int maxYOffset, byte[] maxTriangle);
-
-  /** returns true if the provided triangle matches the query */
-  protected boolean queryMatches(byte[] triangle, ShapeField.DecodedTriangle scratchTriangle, ShapeField.QueryRelation queryRelation) {
-    switch (queryRelation) {
-      case INTERSECTS: return queryIntersects(triangle, scratchTriangle);
-      case WITHIN: return queryContains(triangle, scratchTriangle);
-      case DISJOINT: return queryIntersects(triangle, scratchTriangle) == false;
-      default: throw new IllegalArgumentException("Unsupported query type :[" + queryRelation + "]");
-    }
-  }
-
-  /** returns true if the provided triangle intersects the query */
-  protected abstract boolean queryIntersects(byte[] triangle, ShapeField.DecodedTriangle scratchTriangle);
-
-  /** returns true if the provided triangle is within the query */
-  protected abstract boolean queryContains(byte[] triangle, ShapeField.DecodedTriangle scratchTriangle);
-
-  /** Return the within relationship between the query and the indexed shape.*/
-  protected abstract Component2D.WithinRelation queryWithin(byte[] triangle, ShapeField.DecodedTriangle scratchTriangle);
-
-  /** relates a range of triangles (internal node) to the query */
-  protected Relation relateRangeToQuery(byte[] minTriangle, byte[] maxTriangle, QueryRelation queryRelation) {
-    // compute bounding box of internal node
-    final Relation r = relateRangeBBoxToQuery(ShapeField.BYTES, 0, minTriangle, 3 * ShapeField.BYTES, 2 * ShapeField.BYTES, maxTriangle);
-    if (queryRelation == QueryRelation.DISJOINT) {
-      return transposeRelation(r);
-    }
-    return r;
-  }
-
-  @Override
-  public void visit(QueryVisitor visitor) {
-    if (visitor.acceptField(field)) {
-      visitor.visitLeaf(this);
-    }
-  }
-
-  @Override
-  public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) {
-    final ShapeQuery query = this;
-    return new ConstantScoreWeight(query, boost) {
-
-      @Override
-      public Scorer scorer(LeafReaderContext context) throws IOException {
-        final ScorerSupplier scorerSupplier = scorerSupplier(context);
-        if (scorerSupplier == null) {
-          return null;
-        }
-        return scorerSupplier.get(Long.MAX_VALUE);
-      }
-
-      @Override
-      public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
-        final LeafReader reader = context.reader();
-        final PointValues values = reader.getPointValues(field);
-        if (values == null) {
-          // No docs in this segment had any points fields
-          return null;
-        }
-        final FieldInfo fieldInfo = reader.getFieldInfos().fieldInfo(field);
-        if (fieldInfo == null) {
-          // No docs in this segment indexed this field at all
-          return null;
-        }
-
-        final Weight weight = this;
-        final Relation rel = relateRangeToQuery(values.getMinPackedValue(), values.getMaxPackedValue(), queryRelation);
-        if (rel == Relation.CELL_OUTSIDE_QUERY || (rel == Relation.CELL_INSIDE_QUERY && queryRelation == QueryRelation.CONTAINS)) {
-          // no documents match the query
-          return null;
-        } else if (values.getDocCount() == reader.maxDoc() && rel == Relation.CELL_INSIDE_QUERY) {
-          // all documents match the query
-          return new ScorerSupplier() {
-            @Override
-            public Scorer get(long leadCost) {
-              return new ConstantScoreScorer(weight, score(), scoreMode, DocIdSetIterator.all(reader.maxDoc()));
-            }
-
-            @Override
-            public long cost() {
-              return reader.maxDoc();
-            }
-          };
-        } else {
-          if (queryRelation != QueryRelation.INTERSECTS
-              && queryRelation != QueryRelation.CONTAINS
-              && hasAnyHits(query, values) == false) {
-            // First we check if we have any hits so we are fast in the adversarial case where
-            // the shape does not match any documents and we are in the dense case
-            return null;
-          }
-          // walk the tree to get matching documents
-          return new RelationScorerSupplier(values, ShapeQuery.this) {
-            @Override
-            public Scorer get(long leadCost) throws IOException {
-              return getScorer(reader, weight, score(), scoreMode);
-            }
-          };
-        }
-      }
-
-      @Override
-      public boolean isCacheable(LeafReaderContext ctx) {
-        return true;
-      }
-    };
-  }
-
-  /** returns the field name */
-  public String getField() {
-    return field;
-  }
-
-  /** returns the query relation */
-  public QueryRelation getQueryRelation() {
-    return queryRelation;
-  }
-
-  @Override
-  public int hashCode() {
-    int hash = classHash();
-    hash = 31 * hash + field.hashCode();
-    hash = 31 * hash + queryRelation.hashCode();
-    return hash;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    return sameClassAs(o) && equalsTo(o);
-  }
-
-  /** class specific equals check */
-  protected boolean equalsTo(Object o) {
-    return Objects.equals(field, ((ShapeQuery)o).field) && this.queryRelation == ((ShapeQuery)o).queryRelation;
-  }
-
-  /** transpose the relation; INSIDE becomes OUTSIDE, OUTSIDE becomes INSIDE, CROSSES remains unchanged */
-  private static Relation transposeRelation(Relation r) {
-    if (r == Relation.CELL_INSIDE_QUERY) {
-      return Relation.CELL_OUTSIDE_QUERY;
-    } else if (r == Relation.CELL_OUTSIDE_QUERY) {
-      return Relation.CELL_INSIDE_QUERY;
-    }
-    return Relation.CELL_CROSSES_QUERY;
-  }
-
-  /** utility class for implementing constant score logic specific to INTERSECT, WITHIN, and DISJOINT */
-  private static abstract class RelationScorerSupplier extends ScorerSupplier {
-    final private PointValues values;
-    final private ShapeQuery query;
-    private long cost = -1;
-
-    RelationScorerSupplier(final PointValues values, final ShapeQuery query) {
-      this.values = values;
-      this.query = query;
-    }
-
-    protected Scorer getScorer(final LeafReader reader, final Weight weight, final float boost, final ScoreMode scoreMode) throws IOException {
-      switch (query.getQueryRelation()) {
-        case INTERSECTS: return getSparseScorer(reader, weight, boost, scoreMode);
-        case WITHIN:
-        case DISJOINT: return getDenseScorer(reader, weight, boost, scoreMode);
-        case CONTAINS: return getContainsDenseScorer(reader, weight, boost, scoreMode);
-        default: throw new IllegalArgumentException("Unsupported query type :[" + query.getQueryRelation() + "]");
-      }
-    }
-
-    /** Scorer used for INTERSECTS **/
-    private Scorer getSparseScorer(final LeafReader reader, final Weight weight, final float boost, final ScoreMode scoreMode) throws IOException {
-      if (values.getDocCount() == reader.maxDoc()
-          && values.getDocCount() == values.size()
-          && cost() > reader.maxDoc() / 2) {
-        // If all docs have exactly one value and the cost is greater
-        // than half the leaf size then maybe we can make things faster
-        // by computing the set of documents that do NOT match the query
-        final FixedBitSet result = new FixedBitSet(reader.maxDoc());
-        result.set(0, reader.maxDoc());
-        final long[] cost = new long[]{reader.maxDoc()};
-        values.intersect(getInverseDenseVisitor(query, result, cost));
-        final DocIdSetIterator iterator = new BitSetIterator(result, cost[0]);
-        return new ConstantScoreScorer(weight, boost, scoreMode, iterator);
-      }
-      if (values.getDocCount() < (values.size() >>> 2)) {
-        // we use a dense structure so we can skip already visited documents
-        final FixedBitSet result = new FixedBitSet(reader.maxDoc());
-        final long[] cost = new long[]{0};
-        values.intersect(getIntersectsDenseVisitor(query, result, cost));
-        assert cost[0] > 0 || result.cardinality() == 0;
-        final DocIdSetIterator iterator = cost[0] == 0 ? DocIdSetIterator.empty() : new BitSetIterator(result, cost[0]);
-        return new ConstantScoreScorer(weight, boost, scoreMode, iterator);
-      } else {
-        final DocIdSetBuilder docIdSetBuilder = new DocIdSetBuilder(reader.maxDoc(), values, query.getField());
-        values.intersect(getSparseVisitor(query, docIdSetBuilder));
-        final DocIdSetIterator iterator = docIdSetBuilder.build().iterator();
-        return new ConstantScoreScorer(weight, boost, scoreMode, iterator);
-      }
-    }
-
-    /** Scorer used for WITHIN and DISJOINT **/
-    private Scorer getDenseScorer(LeafReader reader, Weight weight, final float boost, ScoreMode scoreMode) throws IOException {
-      final FixedBitSet result = new FixedBitSet(reader.maxDoc());
-      final long[] cost;
-      if (values.getDocCount() == reader.maxDoc()) {
-        cost = new long[]{values.size()};
-        // In this case we can spare one visit to the tree, all documents
-        // are potential matches
-        result.set(0, reader.maxDoc());
-        // Remove false positives
-        values.intersect(getInverseDenseVisitor(query, result, cost));
-      } else {
-        cost = new long[]{0};
-        // Get potential  documents.
-        final FixedBitSet excluded = new FixedBitSet(reader.maxDoc());
-        values.intersect(getDenseVisitor(query, result, excluded, cost));
-        result.andNot(excluded);
-        // Remove false positives, we only care about the inner nodes as intersecting
-        // leaf nodes have been already taken into account. Unfortunately this
-        // process still reads the leaf nodes.
-        values.intersect(getShallowInverseDenseVisitor(query, result));
-      }
-      assert cost[0] > 0 || result.cardinality() == 0;
-      final DocIdSetIterator iterator = cost[0] == 0 ? DocIdSetIterator.empty() : new BitSetIterator(result, cost[0]);
-      return new ConstantScoreScorer(weight, boost, scoreMode, iterator);
-    }
-
-    private Scorer getContainsDenseScorer(LeafReader reader, Weight weight, final float boost, ScoreMode scoreMode) throws IOException {
-      final FixedBitSet result = new FixedBitSet(reader.maxDoc());
-      final long[] cost = new long[]{0};
-      // Get potential  documents.
-      final FixedBitSet excluded = new FixedBitSet(reader.maxDoc());
-      values.intersect(getContainsDenseVisitor(query, result, excluded, cost));
-      result.andNot(excluded);
-      assert cost[0] > 0 || result.cardinality() == 0;
-      final DocIdSetIterator iterator = cost[0] == 0 ? DocIdSetIterator.empty() : new BitSetIterator(result, cost[0]);
-      return new ConstantScoreScorer(weight, boost, scoreMode, iterator);
-    }
-
-    @Override
-    public long cost() {
-      if (cost == -1) {
-        // Computing the cost may be expensive, so only do it if necessary
-        cost = values.estimateDocCount(getEstimateVisitor(query));
-        assert cost >= 0;
-      }
-      return cost;
-    }
-  }
-
-  /** create a visitor for calculating point count estimates for the provided relation */
-  private static IntersectVisitor getEstimateVisitor(final ShapeQuery query) {
-    return new IntersectVisitor() {
-      @Override
-      public void visit(int docID) {
-        throw new UnsupportedOperationException();
-      }
-
-      @Override
-      public void visit(int docID, byte[] t) {
-        throw new UnsupportedOperationException();
-      }
-
-      @Override
-      public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
-        return query.relateRangeToQuery(minTriangle, maxTriangle, query.getQueryRelation());
-      }
-    };
-  }
-
-  /** create a visitor that adds documents that match the query using a sparse bitset. (Used by INTERSECT
-   * when the number of docs <= 4 * number of points ) */
-  private static IntersectVisitor getSparseVisitor(final ShapeQuery query, final DocIdSetBuilder result) {
-    return new IntersectVisitor() {
-      final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
-      DocIdSetBuilder.BulkAdder adder;
-
-      @Override
-      public void grow(int count) {
-        adder = result.grow(count);
-      }
-
-      @Override
-      public void visit(int docID) {
-        adder.add(docID);
-      }
-
-      @Override
-      public void visit(int docID, byte[] t) {
-        if (query.queryMatches(t, scratchTriangle, query.getQueryRelation())) {
-          visit(docID);
-        }
-      }
-
-      @Override
-      public void visit(DocIdSetIterator iterator, byte[] t) throws IOException {
-        if (query.queryMatches(t, scratchTriangle, query.getQueryRelation())) {
-          int docID;
-          while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
-            visit(docID);
-          }
-        }
-      }
-
-      @Override
-      public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
-        return query.relateRangeToQuery(minTriangle, maxTriangle, query.getQueryRelation());
-      }
-    };
-  }
-
-  /** Scorer used for INTERSECTS when the number of points > 4 * number of docs **/
-  private static IntersectVisitor getIntersectsDenseVisitor(final ShapeQuery query, final FixedBitSet result, final long[] cost) {
-    return new IntersectVisitor() {
-      final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
-
-      @Override
-      public void visit(int docID) {
-        result.set(docID);
-        cost[0]++;
-      }
-
-      @Override
-      public void visit(int docID, byte[] t) {
-        if (result.get(docID) == false) {
-          if (query.queryMatches(t, scratchTriangle, query.getQueryRelation())) {
-            visit(docID);
-          }
-        }
-      }
-
-      @Override
-      public void visit(DocIdSetIterator iterator, byte[] t) throws IOException {
-        if (query.queryMatches(t, scratchTriangle, query.getQueryRelation())) {
-          int docID;
-          while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
-              visit(docID);
-          }
-        }
-      }
-
-      @Override
-      public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
-        return query.relateRangeToQuery(minTriangle, maxTriangle, query.getQueryRelation());
-      }
-    };
-  }
-
-  /** create a visitor that adds documents that match the query using a dense bitset; used with WITHIN & DISJOINT */
-  private static IntersectVisitor getDenseVisitor(final ShapeQuery query, final FixedBitSet result, final FixedBitSet excluded, final long[] cost) {
-    return new IntersectVisitor() {
-      final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
-
-      @Override
-      public void visit(int docID) {
-        result.set(docID);
-        cost[0]++;
-      }
-
-      @Override
-      public void visit(int docID, byte[] t) {
-        if (excluded.get(docID) == false) {
-          if (query.queryMatches(t, scratchTriangle, query.getQueryRelation())) {
-            visit(docID);
-          } else {
-            excluded.set(docID);
-          }
-        }
-      }
-
-      @Override
-      public void visit(DocIdSetIterator iterator, byte[] t) throws IOException {
-        boolean matches = query.queryMatches(t, scratchTriangle, query.getQueryRelation());
-        int docID;
-        while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
-          if (matches) {
-            visit(docID);
-          } else {
-            excluded.set(docID);
-          }
-        }
-      }
-
-      @Override
-      public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
-        return query.relateRangeToQuery(minTriangle, maxTriangle, query.getQueryRelation());
-      }
-    };
-  }
-
-  /** create a visitor that adds documents that match the query using a dense bitset; used with CONTAINS */
-  private static IntersectVisitor getContainsDenseVisitor(final ShapeQuery query, final FixedBitSet result, final FixedBitSet excluded, final long[] cost) {
-    return new IntersectVisitor() {
-      final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
-
-      @Override
-      public void visit(int docID) {
-        excluded.set(docID);
-      }
-
-      @Override
-      public void visit(int docID, byte[] t) {
-        if (excluded.get(docID) == false) {
-          Component2D.WithinRelation within = query.queryWithin(t, scratchTriangle);
-          if (within == Component2D.WithinRelation.CANDIDATE) {
-            cost[0]++;
-            result.set(docID);
-          } else if (within == Component2D.WithinRelation.NOTWITHIN) {
-            excluded.set(docID);
-          }
-        }
-      }
-
-      @Override
-      public void visit(DocIdSetIterator iterator, byte[] t) throws IOException {
-        Component2D.WithinRelation within = query.queryWithin(t, scratchTriangle);
-        int docID;
-        while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
-          if (within == Component2D.WithinRelation.CANDIDATE) {
-            cost[0]++;
-            result.set(docID);
-          } else if (within == Component2D.WithinRelation.NOTWITHIN) {
-            excluded.set(docID);
-          }
-        }
-      }
-
-      @Override
-      public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
-        return query.relateRangeToQuery(minTriangle, maxTriangle, query.getQueryRelation());
-      }
-    };
-  }
-
-  /** create a visitor that clears documents that do not match the polygon query using a dense bitset; used with WITHIN & DISJOINT */
-  private static IntersectVisitor getInverseDenseVisitor(final ShapeQuery query, final FixedBitSet result, final long[] cost) {
-    return new IntersectVisitor() {
-      final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
-
-      @Override
-      public void visit(int docID) {
-        result.clear(docID);
-        cost[0]--;
-      }
-
-      @Override
-      public void visit(int docID, byte[] packedTriangle) {
-        if (result.get(docID)) {
-          if (query.queryMatches(packedTriangle, scratchTriangle, query.getQueryRelation()) == false) {
-            visit(docID);
-          }
-        }
-      }
-
-      @Override
-      public void visit(DocIdSetIterator iterator, byte[] t) throws IOException {
-        if (query.queryMatches(t, scratchTriangle, query.getQueryRelation()) == false) {
-          int docID;
-          while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
-            visit(docID);
-          }
-        }
-      }
-
-      @Override
-      public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
-        return transposeRelation(query.relateRangeToQuery(minPackedValue, maxPackedValue, query.getQueryRelation()));
-      }
-    };
-  }
-
-  /** create a visitor that clears documents that do not match the polygon query using a dense bitset; used with WITHIN & DISJOINT.
-   * This visitor only takes into account inner nodes */
-  private static IntersectVisitor getShallowInverseDenseVisitor(final ShapeQuery query, final FixedBitSet result) {
-    return new IntersectVisitor() {
-
-      @Override
-      public void visit(int docID) {
-        result.clear(docID);
-      }
-
-      @Override
-      public void visit(int docID, byte[] packedTriangle) {
-        //NO-OP
-      }
-
-      @Override
-      public void visit(DocIdSetIterator iterator, byte[] t) {
-        //NO-OP
-      }
-
-      @Override
-      public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
-        return transposeRelation(query.relateRangeToQuery(minPackedValue, maxPackedValue, query.getQueryRelation()));
-      }
-    };
-  }
-
-  /** Return true if the query matches at least one document. It creates a visitor that terminates as soon as one or more docs
-   * are matched. */
-  private static boolean hasAnyHits(final ShapeQuery query, final PointValues values) throws IOException {
-    try {
-      values.intersect(new IntersectVisitor() {
-        final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
-
-        @Override
-        public void visit(int docID) {
-          throw new CollectionTerminatedException();
-        }
-
-        @Override
-        public void visit(int docID, byte[] t) {
-          if (query.queryMatches(t, scratchTriangle, query.getQueryRelation())) {
-            throw new CollectionTerminatedException();
-          }
-        }
-
-        @Override
-        public void visit(DocIdSetIterator iterator, byte[] t) {
-          if (query.queryMatches(t, scratchTriangle, query.getQueryRelation())) {
-            throw new CollectionTerminatedException();
-          }
-        }
-
-        @Override
-        public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
-          Relation rel = query.relateRangeToQuery(minPackedValue, maxPackedValue, query.getQueryRelation());
-          if (rel == Relation.CELL_INSIDE_QUERY) {
-            throw new CollectionTerminatedException();
-          }
-          return rel;
-        }
-      });
-    } catch (CollectionTerminatedException e) {
-      return true;
-    }
-    return false;
-  }
-}
diff --git a/lucene/core/src/java/org/apache/lucene/document/SpatialQuery.java b/lucene/core/src/java/org/apache/lucene/document/SpatialQuery.java
new file mode 100644
index 0000000..f970862
--- /dev/null
+++ b/lucene/core/src/java/org/apache/lucene/document/SpatialQuery.java
@@ -0,0 +1,722 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.document;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import org.apache.lucene.document.ShapeField.QueryRelation;
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.index.PointValues.IntersectVisitor;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.search.CollectionTerminatedException;
+import org.apache.lucene.search.ConstantScoreScorer;
+import org.apache.lucene.search.ConstantScoreWeight;
+import org.apache.lucene.search.DocIdSetIterator;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
+import org.apache.lucene.search.ScoreMode;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.ScorerSupplier;
+import org.apache.lucene.search.Weight;
+import org.apache.lucene.util.BitSetIterator;
+import org.apache.lucene.util.DocIdSetBuilder;
+import org.apache.lucene.util.FixedBitSet;
+
+/**
+ * Base query class for all spatial geometries: {@link LatLonShape}, {@link LatLonPoint} and {@link
+ * XYShape}. In order to create a query, use the factory methods on those classes.
+ */
+abstract class SpatialQuery extends Query {
+  /** field name */
+  final String field;
+  /**
+   * query relation disjoint: {@link QueryRelation#DISJOINT}, intersects: {@link
+   * QueryRelation#INTERSECTS}, within: {@link QueryRelation#DISJOINT}, contains: {@link
+   * QueryRelation#CONTAINS}
+   */
+  final QueryRelation queryRelation;
+
+  protected SpatialQuery(String field, final QueryRelation queryRelation) {
+    if (field == null) {
+      throw new IllegalArgumentException("field must not be null");
+    }
+    if (queryRelation == null) {
+      throw new IllegalArgumentException("queryRelation must not be null");
+    }
+    this.field = field;
+    this.queryRelation = queryRelation;
+  }
+
+  /**
+   * returns the spatial visitor to be used for this query. Called before generating the query
+   * {@link Weight}
+   */
+  protected abstract SpatialVisitor getSpatialVisitor();
+
+  /** Visitor used for walking the BKD tree. */
+  protected abstract static class SpatialVisitor {
+    /** relates a range of points (internal node) to the query */
+    protected abstract Relation relate(byte[] minPackedValue, byte[] maxPackedValue);
+
+    /** Gets a intersects predicate. Called when constructing a {@link Scorer} */
+    protected abstract Predicate<byte[]> intersects();
+
+    /** Gets a within predicate. Called when constructing a {@link Scorer} */
+    protected abstract Predicate<byte[]> within();
+
+    /** Gets a contains function. Called when constructing a {@link Scorer} */
+    protected abstract Function<byte[], Component2D.WithinRelation> contains();
+
+    private Predicate<byte[]> containsPredicate() {
+      final Function<byte[], Component2D.WithinRelation> contains = contains();
+      return bytes -> contains.apply(bytes) == Component2D.WithinRelation.CANDIDATE;
+    }
+
+    private BiFunction<byte[], byte[], Relation> getInnerFunction(
+            ShapeField.QueryRelation queryRelation) {
+      if (queryRelation == QueryRelation.DISJOINT) {
+        return (minPackedValue, maxPackedValue) ->
+                transposeRelation(relate(minPackedValue, maxPackedValue));
+      }
+      return (minPackedValue, maxPackedValue) -> relate(minPackedValue, maxPackedValue);
+    }
+
+    private Predicate<byte[]> getLeafPredicate(ShapeField.QueryRelation queryRelation) {
+      switch (queryRelation) {
+        case INTERSECTS:
+          return intersects();
+        case WITHIN:
+          return within();
+        case DISJOINT:
+          return intersects().negate();
+        case CONTAINS:
+          return containsPredicate();
+        default:
+          throw new IllegalArgumentException("Unsupported query type :[" + queryRelation + "]");
+      }
+    }
+  }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
+  public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) {
+    final SpatialQuery query = this;
+    final SpatialVisitor spatialVisitor = getSpatialVisitor();
+    return new ConstantScoreWeight(query, boost) {
+
+      @Override
+      public Scorer scorer(LeafReaderContext context) throws IOException {
+        final ScorerSupplier scorerSupplier = scorerSupplier(context);
+        if (scorerSupplier == null) {
+          return null;
+        }
+        return scorerSupplier.get(Long.MAX_VALUE);
+      }
+
+      @Override
+      public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
+        final LeafReader reader = context.reader();
+        final PointValues values = reader.getPointValues(field);
+        if (values == null) {
+          // No docs in this segment had any points fields
+          return null;
+        }
+        final FieldInfo fieldInfo = reader.getFieldInfos().fieldInfo(field);
+        if (fieldInfo == null) {
+          // No docs in this segment indexed this field at all
+          return null;
+        }
+        final Weight weight = this;
+        final Relation rel =
+                spatialVisitor
+                        .getInnerFunction(queryRelation)
+                        .apply(values.getMinPackedValue(), values.getMaxPackedValue());
+        if (rel == Relation.CELL_OUTSIDE_QUERY
+                || (rel == Relation.CELL_INSIDE_QUERY && queryRelation == QueryRelation.CONTAINS)) {
+          // no documents match the query
+          return null;
+        } else if (values.getDocCount() == reader.maxDoc() && rel == Relation.CELL_INSIDE_QUERY) {
+          // all documents match the query
+          return new ScorerSupplier() {
+            @Override
+            public Scorer get(long leadCost) {
+              return new ConstantScoreScorer(
+                      weight, score(), scoreMode, DocIdSetIterator.all(reader.maxDoc()));
+            }
+
+            @Override
+            public long cost() {
+              return reader.maxDoc();
+            }
+          };
+        } else {
+          if (queryRelation != QueryRelation.INTERSECTS
+                  && queryRelation != QueryRelation.CONTAINS
+                  && values.getDocCount() != values.size()
+                  && hasAnyHits(spatialVisitor, queryRelation, values) == false) {
+            // First we check if we have any hits so we are fast in the adversarial case where
+            // the shape does not match any documents and we are in the dense case
+            return null;
+          }
+          // walk the tree to get matching documents
+          return new RelationScorerSupplier(values, spatialVisitor, queryRelation, field) {
+            @Override
+            public Scorer get(long leadCost) throws IOException {
+              return getScorer(reader, weight, score(), scoreMode);
+            }
+          };
+        }
+      }
+
+      @Override
+      public boolean isCacheable(LeafReaderContext ctx) {
+        return true;
+      }
+    };
+  }
+
+  /** returns the field name */
+  public String getField() {
+    return field;
+  }
+
+  /** returns the query relation */
+  public QueryRelation getQueryRelation() {
+    return queryRelation;
+  }
+
+  @Override
+  public int hashCode() {
+    int hash = classHash();
+    hash = 31 * hash + field.hashCode();
+    hash = 31 * hash + queryRelation.hashCode();
+    return hash;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return sameClassAs(o) && equalsTo(o);
+  }
+
+  /** class specific equals check */
+  protected boolean equalsTo(Object o) {
+    return Objects.equals(field, ((SpatialQuery) o).field)
+            && this.queryRelation == ((SpatialQuery) o).queryRelation;
+  }
+
+  /**
+   * transpose the relation; INSIDE becomes OUTSIDE, OUTSIDE becomes INSIDE, CROSSES remains
+   * unchanged
+   */
+  protected static Relation transposeRelation(Relation r) {
+    if (r == Relation.CELL_INSIDE_QUERY) {
+      return Relation.CELL_OUTSIDE_QUERY;
+    } else if (r == Relation.CELL_OUTSIDE_QUERY) {
+      return Relation.CELL_INSIDE_QUERY;
+    }
+    return Relation.CELL_CROSSES_QUERY;
+  }
+
+  /**
+   * utility class for implementing constant score logic specific to INTERSECT, WITHIN, and DISJOINT
+   */
+  private abstract static class RelationScorerSupplier extends ScorerSupplier {
+    private final PointValues values;
+    private final SpatialVisitor spatialVisitor;
+    private final QueryRelation queryRelation;
+    private final String field;
+    private long cost = -1;
+
+    RelationScorerSupplier(
+            final PointValues values,
+            SpatialVisitor spatialVisitor,
+            final QueryRelation queryRelation,
+            final String field) {
+      this.values = values;
+      this.spatialVisitor = spatialVisitor;
+      this.queryRelation = queryRelation;
+      this.field = field;
+    }
+
+    protected Scorer getScorer(
+            final LeafReader reader, final Weight weight, final float boost, final ScoreMode scoreMode)
+            throws IOException {
+      switch (queryRelation) {
+        case INTERSECTS:
+          return getSparseScorer(reader, weight, boost, scoreMode);
+        case CONTAINS:
+          return getContainsDenseScorer(reader, weight, boost, scoreMode);
+        case WITHIN:
+        case DISJOINT:
+          return values.getDocCount() == values.size()
+                  ? getSparseScorer(reader, weight, boost, scoreMode)
+                  : getDenseScorer(reader, weight, boost, scoreMode);
+        default:
+          throw new IllegalArgumentException("Unsupported query type :[" + queryRelation + "]");
+      }
+    }
+
+    /** Scorer used for INTERSECTS and single value points */
+    private Scorer getSparseScorer(
+            final LeafReader reader, final Weight weight, final float boost, final ScoreMode scoreMode)
+            throws IOException {
+      if (queryRelation == QueryRelation.DISJOINT
+              && values.getDocCount() == reader.maxDoc()
+              && values.getDocCount() == values.size()
+              && cost() > reader.maxDoc() / 2) {
+        // If all docs have exactly one value and the cost is greater
+        // than half the leaf size then maybe we can make things faster
+        // by computing the set of documents that do NOT match the query
+        final FixedBitSet result = new FixedBitSet(reader.maxDoc());
+        result.set(0, reader.maxDoc());
+        final long[] cost = new long[] {reader.maxDoc()};
+        values.intersect(getInverseDenseVisitor(spatialVisitor, queryRelation, result, cost));
+        final DocIdSetIterator iterator = new BitSetIterator(result, cost[0]);
+        return new ConstantScoreScorer(weight, boost, scoreMode, iterator);
+      } else if (values.getDocCount() < (values.size() >>> 2)) {
+        // we use a dense structure so we can skip already visited documents
+        final FixedBitSet result = new FixedBitSet(reader.maxDoc());
+        final long[] cost = new long[] {0};
+        values.intersect(getIntersectsDenseVisitor(spatialVisitor, queryRelation, result, cost));
+        assert cost[0] > 0 || result.cardinality() == 0;
+        final DocIdSetIterator iterator =
+                cost[0] == 0 ? DocIdSetIterator.empty() : new BitSetIterator(result, cost[0]);
+        return new ConstantScoreScorer(weight, boost, scoreMode, iterator);
+      } else {
+        final DocIdSetBuilder docIdSetBuilder = new DocIdSetBuilder(reader.maxDoc(), values, field);
+        values.intersect(getSparseVisitor(spatialVisitor, queryRelation, docIdSetBuilder));
+        final DocIdSetIterator iterator = docIdSetBuilder.build().iterator();
+        return new ConstantScoreScorer(weight, boost, scoreMode, iterator);
+      }
+    }
+
+    /** Scorer used for WITHIN and DISJOINT */
+    private Scorer getDenseScorer(
+            LeafReader reader, Weight weight, final float boost, ScoreMode scoreMode)
+            throws IOException {
+      final FixedBitSet result = new FixedBitSet(reader.maxDoc());
+      final long[] cost;
+      if (values.getDocCount() == reader.maxDoc()) {
+        cost = new long[] {values.size()};
+        // In this case we can spare one visit to the tree, all documents
+        // are potential matches
+        result.set(0, reader.maxDoc());
+        // Remove false positives
+        values.intersect(getInverseDenseVisitor(spatialVisitor, queryRelation, result, cost));
+      } else {
+        cost = new long[] {0};
+        // Get potential  documents.
+        final FixedBitSet excluded = new FixedBitSet(reader.maxDoc());
+        values.intersect(getDenseVisitor(spatialVisitor, queryRelation, result, excluded, cost));
+        result.andNot(excluded);
+        // Remove false positives, we only care about the inner nodes as intersecting
+        // leaf nodes have been already taken into account. Unfortunately this
+        // process still reads the leaf nodes.
+        values.intersect(getShallowInverseDenseVisitor(spatialVisitor, queryRelation, result));
+      }
+      assert cost[0] > 0 || result.cardinality() == 0;
+      final DocIdSetIterator iterator =
+              cost[0] == 0 ? DocIdSetIterator.empty() : new BitSetIterator(result, cost[0]);
+      return new ConstantScoreScorer(weight, boost, scoreMode, iterator);
+    }
+
+    private Scorer getContainsDenseScorer(
+            LeafReader reader, Weight weight, final float boost, ScoreMode scoreMode)
+            throws IOException {
+      final FixedBitSet result = new FixedBitSet(reader.maxDoc());
+      final long[] cost = new long[] {0};
+      // Get potential  documents.
+      final FixedBitSet excluded = new FixedBitSet(reader.maxDoc());
+      values.intersect(
+              getContainsDenseVisitor(spatialVisitor, queryRelation, result, excluded, cost));
+      result.andNot(excluded);
+      assert cost[0] > 0 || result.cardinality() == 0;
+      final DocIdSetIterator iterator =
+              cost[0] == 0 ? DocIdSetIterator.empty() : new BitSetIterator(result, cost[0]);
+      return new ConstantScoreScorer(weight, boost, scoreMode, iterator);
+    }
+
+    @Override
+    public long cost() {
+      if (cost == -1) {
+        // Computing the cost may be expensive, so only do it if necessary
+        cost = values.estimateDocCount(getEstimateVisitor(spatialVisitor, queryRelation));
+        assert cost >= 0;
+      }
+      return cost;
+    }
+  }
+
+  /** create a visitor for calculating point count estimates for the provided relation */
+  private static IntersectVisitor getEstimateVisitor(
+          final SpatialVisitor spatialVisitor, QueryRelation queryRelation) {
+    final BiFunction<byte[], byte[], Relation> innerFunction =
+            spatialVisitor.getInnerFunction(queryRelation);
+    return new IntersectVisitor() {
+      @Override
+      public void visit(int docID) {
+        throw new UnsupportedOperationException();
+      }
+
+      @Override
+      public void visit(int docID, byte[] t) {
+        throw new UnsupportedOperationException();
+      }
+
+      @Override
+      public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
+        return innerFunction.apply(minTriangle, maxTriangle);
+      }
+    };
+  }
+
+  /**
+   * create a visitor that adds documents that match the query using a sparse bitset. (Used by
+   * INTERSECT when the number of docs <= 4 * number of points )
+   */
+  private static IntersectVisitor getSparseVisitor(
+          final SpatialVisitor spatialVisitor,
+          QueryRelation queryRelation,
+          final DocIdSetBuilder result) {
+    final BiFunction<byte[], byte[], Relation> innerFunction =
+            spatialVisitor.getInnerFunction(queryRelation);
+    final Predicate<byte[]> leafPredicate = spatialVisitor.getLeafPredicate(queryRelation);
+    return new IntersectVisitor() {
+      DocIdSetBuilder.BulkAdder adder;
+
+      @Override
+      public void grow(int count) {
+        adder = result.grow(count);
+      }
+
+      @Override
+      public void visit(int docID) {
+        adder.add(docID);
+      }
+
+      @Override
+      public void visit(int docID, byte[] t) {
+        if (leafPredicate.test(t)) {
+          visit(docID);
+        }
+      }
+
+      @Override
+      public void visit(DocIdSetIterator iterator, byte[] t) throws IOException {
+        if (leafPredicate.test(t)) {
+          int docID;
+          while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
+            visit(docID);
+          }
+        }
+      }
+
+      @Override
+      public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
+        return innerFunction.apply(minTriangle, maxTriangle);
+      }
+    };
+  }
+
+  /** Scorer used for INTERSECTS when the number of points > 4 * number of docs */
+  private static IntersectVisitor getIntersectsDenseVisitor(
+          final SpatialVisitor spatialVisitor,
+          QueryRelation queryRelation,
+          final FixedBitSet result,
+          final long[] cost) {
+    final BiFunction<byte[], byte[], Relation> innerFunction =
+            spatialVisitor.getInnerFunction(queryRelation);
+    final Predicate<byte[]> leafPredicate = spatialVisitor.getLeafPredicate(queryRelation);
+    return new IntersectVisitor() {
+
+      @Override
+      public void visit(int docID) {
+        result.set(docID);
+        cost[0]++;
+      }
+
+      @Override
+      public void visit(int docID, byte[] t) {
+        if (result.get(docID) == false) {
+          if (leafPredicate.test(t)) {
+            visit(docID);
+          }
+        }
+      }
+
+      @Override
+      public void visit(DocIdSetIterator iterator, byte[] t) throws IOException {
+        if (leafPredicate.test(t)) {
+          int docID;
+          while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
+            visit(docID);
+          }
+        }
+      }
+
+      @Override
+      public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
+        return innerFunction.apply(minTriangle, maxTriangle);
+      }
+    };
+  }
+
+  /**
+   * create a visitor that adds documents that match the query using a dense bitset; used with
+   * WITHIN & DISJOINT
+   */
+  private static IntersectVisitor getDenseVisitor(
+          final SpatialVisitor spatialVisitor,
+          final QueryRelation queryRelation,
+          final FixedBitSet result,
+          final FixedBitSet excluded,
+          final long[] cost) {
+    final BiFunction<byte[], byte[], Relation> innerFunction =
+            spatialVisitor.getInnerFunction(queryRelation);
+    final Predicate<byte[]> leafPredicate = spatialVisitor.getLeafPredicate(queryRelation);
+    return new IntersectVisitor() {
+      @Override
+      public void visit(int docID) {
+        result.set(docID);
+        cost[0]++;
+      }
+
+      @Override
+      public void visit(int docID, byte[] t) {
+        if (excluded.get(docID) == false) {
+          if (leafPredicate.test(t)) {
+            visit(docID);
+          } else {
+            excluded.set(docID);
+          }
+        }
+      }
+
+      @Override
+      public void visit(DocIdSetIterator iterator, byte[] t) throws IOException {
+        boolean matches = leafPredicate.test(t);
+        int docID;
+        while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
+          if (matches) {
+            visit(docID);
+          } else {
+            excluded.set(docID);
+          }
+        }
+      }
+
+      @Override
+      public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
+        return innerFunction.apply(minTriangle, maxTriangle);
+      }
+    };
+  }
+
+  /**
+   * create a visitor that adds documents that match the query using a dense bitset; used with
+   * CONTAINS
+   */
+  private static IntersectVisitor getContainsDenseVisitor(
+          final SpatialVisitor spatialVisitor,
+          final QueryRelation queryRelation,
+          final FixedBitSet result,
+          final FixedBitSet excluded,
+          final long[] cost) {
+    final BiFunction<byte[], byte[], Relation> innerFunction =
+            spatialVisitor.getInnerFunction(queryRelation);
+    final Function<byte[], Component2D.WithinRelation> leafFunction = spatialVisitor.contains();
+    return new IntersectVisitor() {
+      @Override
+      public void visit(int docID) {
+        excluded.set(docID);
+      }
+
+      @Override
+      public void visit(int docID, byte[] t) {
+        if (excluded.get(docID) == false) {
+          Component2D.WithinRelation within = leafFunction.apply(t);
+          if (within == Component2D.WithinRelation.CANDIDATE) {
+            cost[0]++;
+            result.set(docID);
+          } else if (within == Component2D.WithinRelation.NOTWITHIN) {
+            excluded.set(docID);
+          }
+        }
+      }
+
+      @Override
+      public void visit(DocIdSetIterator iterator, byte[] t) throws IOException {
+        Component2D.WithinRelation within = leafFunction.apply(t);
+        int docID;
+        while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
+          if (within == Component2D.WithinRelation.CANDIDATE) {
+            cost[0]++;
+            result.set(docID);
+          } else if (within == Component2D.WithinRelation.NOTWITHIN) {
+            excluded.set(docID);
+          }
+        }
+      }
+
+      @Override
+      public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
+        return innerFunction.apply(minTriangle, maxTriangle);
+      }
+    };
+  }
+
+  /**
+   * create a visitor that clears documents that do not match the polygon query using a dense
+   * bitset; used with WITHIN & DISJOINT
+   */
+  private static IntersectVisitor getInverseDenseVisitor(
+          final SpatialVisitor spatialVisitor,
+          final QueryRelation queryRelation,
+          final FixedBitSet result,
+          final long[] cost) {
+    final BiFunction<byte[], byte[], Relation> innerFunction =
+            spatialVisitor.getInnerFunction(queryRelation);
+    final Predicate<byte[]> leafPredicate = spatialVisitor.getLeafPredicate(queryRelation);
+    return new IntersectVisitor() {
+
+      @Override
+      public void visit(int docID) {
+        result.clear(docID);
+        cost[0]--;
+      }
+
+      @Override
+      public void visit(int docID, byte[] packedTriangle) {
+        if (result.get(docID)) {
+          if (leafPredicate.test(packedTriangle) == false) {
+            visit(docID);
+          }
+        }
+      }
+
+      @Override
+      public void visit(DocIdSetIterator iterator, byte[] t) throws IOException {
+        if (leafPredicate.test(t) == false) {
+          int docID;
+          while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
+            visit(docID);
+          }
+        }
+      }
+
+      @Override
+      public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
+        return transposeRelation(innerFunction.apply(minPackedValue, maxPackedValue));
+      }
+    };
+  }
+
+  /**
+   * create a visitor that clears documents that do not match the polygon query using a dense
+   * bitset; used with WITHIN & DISJOINT. This visitor only takes into account inner nodes
+   */
+  private static IntersectVisitor getShallowInverseDenseVisitor(
+          final SpatialVisitor spatialVisitor, QueryRelation queryRelation, final FixedBitSet result) {
+    final BiFunction<byte[], byte[], Relation> innerFunction =
+            spatialVisitor.getInnerFunction(queryRelation);
+    ;
+    return new IntersectVisitor() {
+
+      @Override
+      public void visit(int docID) {
+        result.clear(docID);
+      }
+
+      @Override
+      public void visit(int docID, byte[] packedTriangle) {
+        // NO-OP
+      }
+
+      @Override
+      public void visit(DocIdSetIterator iterator, byte[] t) {
+        // NO-OP
+      }
+
+      @Override
+      public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
+        return transposeRelation(innerFunction.apply(minPackedValue, maxPackedValue));
+      }
+    };
+  }
+
+  /**
+   * Return true if the query matches at least one document. It creates a visitor that terminates as
+   * soon as one or more docs are matched.
+   */
+  private static boolean hasAnyHits(
+          final SpatialVisitor spatialVisitor, QueryRelation queryRelation, final PointValues values)
+          throws IOException {
+    try {
+      final BiFunction<byte[], byte[], Relation> innerFunction =
+              spatialVisitor.getInnerFunction(queryRelation);
+      final Predicate<byte[]> leafPredicate = spatialVisitor.getLeafPredicate(queryRelation);
+      values.intersect(
+              new IntersectVisitor() {
+
+                @Override
+                public void visit(int docID) {
+                  throw new CollectionTerminatedException();
+                }
+
+                @Override
+                public void visit(int docID, byte[] t) {
+                  if (leafPredicate.test(t)) {
+                    throw new CollectionTerminatedException();
+                  }
+                }
+
+                @Override
+                public void visit(DocIdSetIterator iterator, byte[] t) {
+                  if (leafPredicate.test(t)) {
+                    throw new CollectionTerminatedException();
+                  }
+                }
+
+                @Override
+                public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
+                  Relation rel = innerFunction.apply(minPackedValue, maxPackedValue);
+                  if (rel == Relation.CELL_INSIDE_QUERY) {
+                    throw new CollectionTerminatedException();
+                  }
+                  return rel;
+                }
+              });
+    } catch (CollectionTerminatedException e) {
+      return true;
+    }
+    return false;
+  }
+}
diff --git a/lucene/core/src/java/org/apache/lucene/document/XYShapeQuery.java b/lucene/core/src/java/org/apache/lucene/document/XYShapeQuery.java
index 14d5436..c577e3f 100644
--- a/lucene/core/src/java/org/apache/lucene/document/XYShapeQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/XYShapeQuery.java
@@ -16,8 +16,11 @@
  */
 package org.apache.lucene.document;
 
-import java.util.Arrays;
+import static org.apache.lucene.geo.XYEncodingUtils.decode;
 
+import java.util.Arrays;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import org.apache.lucene.document.ShapeField.QueryRelation;
 import org.apache.lucene.geo.Component2D;
 import org.apache.lucene.geo.XYEncodingUtils;
@@ -25,158 +28,196 @@ import org.apache.lucene.geo.XYGeometry;
 import org.apache.lucene.index.PointValues.Relation;
 import org.apache.lucene.util.NumericUtils;
 
-import static org.apache.lucene.geo.XYEncodingUtils.decode;
-
 /**
  * Finds all previously indexed cartesian shapes that comply the given {@link QueryRelation} with
  * the specified array of {@link XYGeometry}.
  *
  * <p>The field must be indexed using {@link XYShape#createIndexableFields} added per document.
- **/
-final class XYShapeQuery extends ShapeQuery {
-  final XYGeometry[] geometries;
-  final private Component2D component2D;
-
-  /**
-   * Creates a query that matches all indexed shapes to the provided polygons
-   */
-  XYShapeQuery(String field, QueryRelation queryRelation, XYGeometry... geometries) {
-    super(field, queryRelation);
-    this.component2D = XYGeometry.create(geometries);
-    this.geometries = geometries.clone();
-  }
-
-  @Override
-  protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle,
-                                            int maxXOffset, int maxYOffset, byte[] maxTriangle) {
-
-    double minY = XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(minTriangle, minYOffset));
-    double minX = XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(minTriangle, minXOffset));
-    double maxY = XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(maxTriangle, maxYOffset));
-    double maxX = XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(maxTriangle, maxXOffset));
-
-    // check internal node against query
-    return component2D.relate(minX, maxX, minY, maxY);
-  }
-
-  @Override
-  protected boolean queryIntersects(byte[] t, ShapeField.DecodedTriangle scratchTriangle) {
-    ShapeField.decodeTriangle(t, scratchTriangle);
-
-    switch (scratchTriangle.type) {
-      case POINT: {
-        double y = decode(scratchTriangle.aY);
-        double x = decode(scratchTriangle.aX);
-        return component2D.contains(x, y);
-      }
-      case LINE: {
-        double aY = decode(scratchTriangle.aY);
-        double aX = decode(scratchTriangle.aX);
-        double bY = decode(scratchTriangle.bY);
-        double bX = decode(scratchTriangle.bX);
-        return component2D.intersectsLine(aX, aY, bX, bY);
-      }
-      case TRIANGLE: {
-        double aY = decode(scratchTriangle.aY);
-        double aX = decode(scratchTriangle.aX);
-        double bY = decode(scratchTriangle.bY);
-        double bX = decode(scratchTriangle.bX);
-        double cY = decode(scratchTriangle.cY);
-        double cX = decode(scratchTriangle.cX);
-        return component2D.intersectsTriangle(aX, aY, bX, bY, cX, cY);
-      }
-      default: throw new IllegalArgumentException("Unsupported triangle type :[" + scratchTriangle.type + "]");
+ */
+final class XYShapeQuery extends SpatialQuery {
+    final XYGeometry[] geometries;
+    private final Component2D component2D;
+
+    /** Creates a query that matches all indexed shapes to the provided polygons */
+    XYShapeQuery(String field, QueryRelation queryRelation, XYGeometry... geometries) {
+        super(field, queryRelation);
+        this.component2D = XYGeometry.create(geometries);
+        this.geometries = geometries.clone();
     }
-  }
-
-  @Override
-  protected boolean queryContains(byte[] t, ShapeField.DecodedTriangle scratchTriangle) {
-    ShapeField.decodeTriangle(t, scratchTriangle);
-
-    switch (scratchTriangle.type) {
-      case POINT: {
-        double y = decode(scratchTriangle.aY);
-        double x = decode(scratchTriangle.aX);
-        return component2D.contains(x, y);
-      }
-      case LINE: {
-        double aY = decode(scratchTriangle.aY);
-        double aX = decode(scratchTriangle.aX);
-        double bY = decode(scratchTriangle.bY);
-        double bX = decode(scratchTriangle.bX);
-        return component2D.containsLine(aX, aY, bX, bY);
-      }
-      case TRIANGLE: {
-        double aY = decode(scratchTriangle.aY);
-        double aX = decode(scratchTriangle.aX);
-        double bY = decode(scratchTriangle.bY);
-        double bX = decode(scratchTriangle.bX);
-        double cY = decode(scratchTriangle.cY);
-        double cX = decode(scratchTriangle.cX);
-        return component2D.containsTriangle(aX, aY, bX, bY, cX, cY);
-      }
-      default: throw new IllegalArgumentException("Unsupported triangle type :[" + scratchTriangle.type + "]");
+
+    @Override
+    protected SpatialVisitor getSpatialVisitor() {
+        return new SpatialVisitor() {
+            @Override
+            protected Relation relate(byte[] minTriangle, byte[] maxTriangle) {
+
+                double minY = XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(minTriangle, 0));
+                double minX =
+                        XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(minTriangle, ShapeField.BYTES));
+                double maxY =
+                        XYEncodingUtils.decode(
+                                NumericUtils.sortableBytesToInt(maxTriangle, 2 * ShapeField.BYTES));
+                double maxX =
+                        XYEncodingUtils.decode(
+                                NumericUtils.sortableBytesToInt(maxTriangle, 3 * ShapeField.BYTES));
+
+                // check internal node against query
+                return component2D.relate(minX, maxX, minY, maxY);
+            }
+
+            @Override
+            protected Predicate<byte[]> intersects() {
+                final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
+                return triangle -> {
+                    ShapeField.decodeTriangle(triangle, scratchTriangle);
+
+                    switch (scratchTriangle.type) {
+                        case POINT:
+                        {
+                            double y = decode(scratchTriangle.aY);
+                            double x = decode(scratchTriangle.aX);
+                            return component2D.contains(x, y);
+                        }
+                        case LINE:
+                        {
+                            double aY = decode(scratchTriangle.aY);
+                            double aX = decode(scratchTriangle.aX);
+                            double bY = decode(scratchTriangle.bY);
+                            double bX = decode(scratchTriangle.bX);
+                            return component2D.intersectsLine(aX, aY, bX, bY);
+                        }
+                        case TRIANGLE:
+                        {
+                            double aY = decode(scratchTriangle.aY);
+                            double aX = decode(scratchTriangle.aX);
+                            double bY = decode(scratchTriangle.bY);
+                            double bX = decode(scratchTriangle.bX);
+                            double cY = decode(scratchTriangle.cY);
+                            double cX = decode(scratchTriangle.cX);
+                            return component2D.intersectsTriangle(aX, aY, bX, bY, cX, cY);
+                        }
+                        default:
+                            throw new IllegalArgumentException(
+                                    "Unsupported triangle type :[" + scratchTriangle.type + "]");
+                    }
+                };
+            }
+
+            @Override
+            protected Predicate<byte[]> within() {
+                final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
+                return triangle -> {
+                    ShapeField.decodeTriangle(triangle, scratchTriangle);
+
+                    switch (scratchTriangle.type) {
+                        case POINT:
+                        {
+                            double y = decode(scratchTriangle.aY);
+                            double x = decode(scratchTriangle.aX);
+                            return component2D.contains(x, y);
+                        }
+                        case LINE:
+                        {
+                            double aY = decode(scratchTriangle.aY);
+                            double aX = decode(scratchTriangle.aX);
+                            double bY = decode(scratchTriangle.bY);
+                            double bX = decode(scratchTriangle.bX);
+                            return component2D.containsLine(aX, aY, bX, bY);
+                        }
+                        case TRIANGLE:
+                        {
+                            double aY = decode(scratchTriangle.aY);
+                            double aX = decode(scratchTriangle.aX);
+                            double bY = decode(scratchTriangle.bY);
+                            double bX = decode(scratchTriangle.bX);
+                            double cY = decode(scratchTriangle.cY);
+                            double cX = decode(scratchTriangle.cX);
+                            return component2D.containsTriangle(aX, aY, bX, bY, cX, cY);
+                        }
+                        default:
+                            throw new IllegalArgumentException(
+                                    "Unsupported triangle type :[" + scratchTriangle.type + "]");
+                    }
+                };
+            }
+
+            @Override
+            protected Function<byte[], Component2D.WithinRelation> contains() {
+                final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle();
+                return triangle -> {
+                    ShapeField.decodeTriangle(triangle, scratchTriangle);
+
+                    switch (scratchTriangle.type) {
+                        case POINT:
+                        {
+                            double y = decode(scratchTriangle.aY);
+                            double x = decode(scratchTriangle.aX);
+                            return component2D.withinPoint(x, y);
+                        }
+                        case LINE:
+                        {
+                            double aY = decode(scratchTriangle.aY);
+                            double aX = decode(scratchTriangle.aX);
+                            double bY = decode(scratchTriangle.bY);
+                            double bX = decode(scratchTriangle.bX);
+                            return component2D.withinLine(aX, aY, scratchTriangle.ab, bX, bY);
+                        }
+                        case TRIANGLE:
+                        {
+                            double aY = decode(scratchTriangle.aY);
+                            double aX = decode(scratchTriangle.aX);
+                            double bY = decode(scratchTriangle.bY);
+                            double bX = decode(scratchTriangle.bX);
+                            double cY = decode(scratchTriangle.cY);
+                            double cX = decode(scratchTriangle.cX);
+                            return component2D.withinTriangle(
+                                    aX,
+                                    aY,
+                                    scratchTriangle.ab,
+                                    bX,
+                                    bY,
+                                    scratchTriangle.bc,
+                                    cX,
+                                    cY,
+                                    scratchTriangle.ca);
+                        }
+                        default:
+                            throw new IllegalArgumentException(
+                                    "Unsupported triangle type :[" + scratchTriangle.type + "]");
+                    }
+                };
+            }
+        };
     }
-  }
-
-  @Override
-  protected Component2D.WithinRelation queryWithin(byte[] t, ShapeField.DecodedTriangle scratchTriangle) {
-    ShapeField.decodeTriangle(t, scratchTriangle);
-
-    switch (scratchTriangle.type) {
-      case POINT: {
-        double y = decode(scratchTriangle.aY);
-        double x = decode(scratchTriangle.aX);
-        return component2D.withinPoint(x, y);
-      }
-      case LINE: {
-        double aY = decode(scratchTriangle.aY);
-        double aX = decode(scratchTriangle.aX);
-        double bY = decode(scratchTriangle.bY);
-        double bX = decode(scratchTriangle.bX);
-        return component2D.withinLine(aX, aY, scratchTriangle.ab, bX, bY);
-      }
-      case TRIANGLE: {
-        double aY = decode(scratchTriangle.aY);
-        double aX = decode(scratchTriangle.aX);
-        double bY = decode(scratchTriangle.bY);
-        double bX = decode(scratchTriangle.bX);
-        double cY = decode(scratchTriangle.cY);
-        double cX = decode(scratchTriangle.cX);
-        return component2D.withinTriangle(aX, aY, scratchTriangle.ab, bX, bY, scratchTriangle.bc, cX, cY, scratchTriangle.ca);
-      }
-      default: throw new IllegalArgumentException("Unsupported triangle type :[" + scratchTriangle.type + "]");
+
+    @Override
+    public String toString(String field) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName());
+        sb.append(':');
+        if (this.field.equals(field) == false) {
+            sb.append(" field=");
+            sb.append(this.field);
+            sb.append(':');
+        }
+        sb.append("[");
+        for (int i = 0; i < geometries.length; i++) {
+            sb.append(geometries[i].toString());
+            sb.append(',');
+        }
+        sb.append(']');
+        return sb.toString();
     }
-  }
-
-  @Override
-  public String toString(String field) {
-    final StringBuilder sb = new StringBuilder();
-    sb.append(getClass().getSimpleName());
-    sb.append(':');
-    if (this.field.equals(field) == false) {
-      sb.append(" field=");
-      sb.append(this.field);
-      sb.append(':');
+
+    @Override
+    protected boolean equalsTo(Object o) {
+        return super.equalsTo(o) && Arrays.equals(geometries, ((XYShapeQuery) o).geometries);
     }
-    sb.append("[");
-    for (int i = 0; i < geometries.length; i++) {
-      sb.append(geometries[i].toString());
-      sb.append(',');
+
+    @Override
+    public int hashCode() {
+        int hash = super.hashCode();
+        hash = 31 * hash + Arrays.hashCode(geometries);
+        return hash;
     }
-    sb.append(']');
-    return sb.toString();
-  }
-
-  @Override
-  protected boolean equalsTo(Object o) {
-    return super.equalsTo(o) && Arrays.equals(geometries, ((XYShapeQuery)o).geometries);
-  }
-
-  @Override
-  public int hashCode() {
-    int hash = super.hashCode();
-    hash = 31 * hash + Arrays.hashCode(geometries);
-    return hash;
-  }
-}
\ No newline at end of file
+}
diff --git a/lucene/core/src/java/org/apache/lucene/geo/GeoEncodingUtils.java b/lucene/core/src/java/org/apache/lucene/geo/GeoEncodingUtils.java
index da63880..2a6a332 100644
--- a/lucene/core/src/java/org/apache/lucene/geo/GeoEncodingUtils.java
+++ b/lucene/core/src/java/org/apache/lucene/geo/GeoEncodingUtils.java
@@ -16,18 +16,17 @@
  */
 package org.apache.lucene.geo;
 
-import org.apache.lucene.index.PointValues.Relation;
-import org.apache.lucene.util.NumericUtils;
-import org.apache.lucene.util.SloppyMath;
-
 import static org.apache.lucene.geo.GeoUtils.MAX_LAT_INCL;
 import static org.apache.lucene.geo.GeoUtils.MAX_LON_INCL;
-import static org.apache.lucene.geo.GeoUtils.MIN_LON_INCL;
 import static org.apache.lucene.geo.GeoUtils.MIN_LAT_INCL;
+import static org.apache.lucene.geo.GeoUtils.MIN_LON_INCL;
 import static org.apache.lucene.geo.GeoUtils.checkLatitude;
 import static org.apache.lucene.geo.GeoUtils.checkLongitude;
 
 import java.util.function.Function;
+import org.apache.lucene.index.PointValues.Relation;
+import org.apache.lucene.util.NumericUtils;
+import org.apache.lucene.util.SloppyMath;
 
 /**
  * reusable geopoint encoding methods
@@ -38,21 +37,20 @@ public final class GeoEncodingUtils {
   /** number of bits used for quantizing latitude and longitude values */
   public static final short BITS = 32;
 
-  private static final double LAT_SCALE = (0x1L<<BITS)/180.0D;
-  private static final double LAT_DECODE = 1/LAT_SCALE;
-  private static final double LON_SCALE = (0x1L<<BITS)/360.0D;
-  private static final double LON_DECODE = 1/LON_SCALE;
+  private static final double LAT_SCALE = (0x1L << BITS) / 180.0D;
+  private static final double LAT_DECODE = 1 / LAT_SCALE;
+  private static final double LON_SCALE = (0x1L << BITS) / 360.0D;
+  private static final double LON_DECODE = 1 / LON_SCALE;
 
   public static final int MIN_LON_ENCODED = encodeLongitude(MIN_LON_INCL);
   public static final int MAX_LON_ENCODED = encodeLongitude(MAX_LON_INCL);
 
-
   // No instance:
-  private GeoEncodingUtils() {
-  }
+  private GeoEncodingUtils() {}
 
   /**
    * Quantizes double (64 bit) latitude into 32 bits (rounding down: in the direction of -90)
+   *
    * @param latitude latitude value: must be within standard +/-90 coordinate bounds.
    * @return encoded value as a 32-bit {@code int}
    * @throws IllegalArgumentException if latitude is out of bounds
@@ -68,6 +66,7 @@ public final class GeoEncodingUtils {
 
   /**
    * Quantizes double (64 bit) latitude into 32 bits (rounding up: in the direction of +90)
+   *
    * @param latitude latitude value: must be within standard +/-90 coordinate bounds.
    * @return encoded value as a 32-bit {@code int}
    * @throws IllegalArgumentException if latitude is out of bounds
@@ -83,6 +82,7 @@ public final class GeoEncodingUtils {
 
   /**
    * Quantizes double (64 bit) longitude into 32 bits (rounding down: in the direction of -180)
+   *
    * @param longitude longitude value: must be within standard +/-180 coordinate bounds.
    * @return encoded value as a 32-bit {@code int}
    * @throws IllegalArgumentException if longitude is out of bounds
@@ -98,6 +98,7 @@ public final class GeoEncodingUtils {
 
   /**
    * Quantizes double (64 bit) longitude into 32 bits (rounding up: in the direction of +180)
+   *
    * @param longitude longitude value: must be within standard +/-180 coordinate bounds.
    * @return encoded value as a 32-bit {@code int}
    * @throws IllegalArgumentException if longitude is out of bounds
@@ -113,6 +114,7 @@ public final class GeoEncodingUtils {
 
   /**
    * Turns quantized value from {@link #encodeLatitude} back into a double.
+   *
    * @param encoded encoded value: 32-bit quantized value.
    * @return decoded latitude value.
    */
@@ -124,6 +126,7 @@ public final class GeoEncodingUtils {
 
   /**
    * Turns quantized value from byte array back into a double.
+   *
    * @param src byte array containing 4 bytes to decode at {@code offset}
    * @param offset offset into {@code src} to decode from.
    * @return decoded latitude value.
@@ -134,6 +137,7 @@ public final class GeoEncodingUtils {
 
   /**
    * Turns quantized value from {@link #encodeLongitude} back into a double.
+   *
    * @param encoded encoded value: 32-bit quantized value.
    * @return decoded longitude value.
    */
@@ -145,6 +149,7 @@ public final class GeoEncodingUtils {
 
   /**
    * Turns quantized value from byte array back into a double.
+   *
    * @param src byte array containing 4 bytes to decode at {@code offset}
    * @param offset offset into {@code src} to decode from.
    * @return decoded longitude value.
@@ -153,55 +158,83 @@ public final class GeoEncodingUtils {
     return decodeLongitude(NumericUtils.sortableBytesToInt(src, offset));
   }
 
-  /** Create a predicate that checks whether points are within a distance of a given point.
-   *  It works by computing the bounding box around the circle that is defined
-   *  by the given points/distance and splitting it into between 1024 and 4096
-   *  smaller boxes (4096*0.75^2=2304 on average). Then for each sub box, it
-   *  computes the relation between this box and the distance query. Finally at
-   *  search time, it first computes the sub box that the point belongs to,
-   *  most of the time, no distance computation will need to be performed since
-   *  all points from the sub box will either be in or out of the circle.
-   *  @lucene.internal */
-  public static DistancePredicate createDistancePredicate(double lat, double lon, double radiusMeters) {
+  /**
+   * Create a predicate that checks whether points are within a distance of a given point. It works
+   * by computing the bounding box around the circle that is defined by the given points/distance
+   * and splitting it into between 1024 and 4096 smaller boxes (4096*0.75^2=2304 on average). Then
+   * for each sub box, it computes the relation between this box and the distance query. Finally at
+   * search time, it first computes the sub box that the point belongs to, most of the time, no
+   * distance computation will need to be performed since all points from the sub box will either be
+   * in or out of the circle.
+   *
+   * @lucene.internal
+   */
+  public static DistancePredicate createDistancePredicate(
+          double lat, double lon, double radiusMeters) {
     final Rectangle boundingBox = Rectangle.fromPointDistance(lat, lon, radiusMeters);
     final double axisLat = Rectangle.axisLat(lat, radiusMeters);
     final double distanceSortKey = GeoUtils.distanceQuerySortKey(radiusMeters);
-    final Function<Rectangle, Relation> boxToRelation = box -> GeoUtils.relate(
-        box.minLat, box.maxLat, box.minLon, box.maxLon, lat, lon, distanceSortKey, axisLat);
-    final Grid subBoxes = createSubBoxes(boundingBox, boxToRelation);
+    final Function<Rectangle, Relation> boxToRelation =
+            box ->
+                    GeoUtils.relate(
+                            box.minLat, box.maxLat, box.minLon, box.maxLon, lat, lon, distanceSortKey, axisLat);
+    final Grid subBoxes =
+            createSubBoxes(
+                    boundingBox.minLat,
+                    boundingBox.maxLat,
+                    boundingBox.minLon,
+                    boundingBox.maxLon,
+                    boxToRelation);
 
     return new DistancePredicate(
-        subBoxes.latShift, subBoxes.lonShift,
-        subBoxes.latBase, subBoxes.lonBase,
-        subBoxes.maxLatDelta, subBoxes.maxLonDelta,
-        subBoxes.relations,
-        lat, lon, distanceSortKey);
+            subBoxes.latShift,
+            subBoxes.lonShift,
+            subBoxes.latBase,
+            subBoxes.lonBase,
+            subBoxes.maxLatDelta,
+            subBoxes.maxLonDelta,
+            subBoxes.relations,
+            lat,
+            lon,
+            distanceSortKey);
   }
 
-  /** Create a predicate that checks whether points are within a geometry.
-   *  It works the same way as {@link #createDistancePredicate}.
-   *  @lucene.internal */
+  /**
+   * Create a predicate that checks whether points are within a geometry. It works the same way as
+   * {@link #createDistancePredicate}.
+   *
+   * @lucene.internal
+   */
   public static Component2DPredicate createComponentPredicate(Component2D tree) {
-    final Rectangle boundingBox = new Rectangle(tree.getMinY(), tree.getMaxY(), tree.getMinX(), tree.getMaxX());
-    final Function<Rectangle, Relation> boxToRelation = box -> tree.relate(
-        box.minLon, box.maxLon, box.minLat, box.maxLat);
-    final Grid subBoxes = createSubBoxes(boundingBox, boxToRelation);
+    final Function<Rectangle, Relation> boxToRelation =
+            box -> tree.relate(box.minLon, box.maxLon, box.minLat, box.maxLat);
+    final Grid subBoxes =
+            createSubBoxes(
+                    tree.getMinY(), tree.getMaxY(), tree.getMinX(), tree.getMaxX(), boxToRelation);
 
     return new Component2DPredicate(
-        subBoxes.latShift, subBoxes.lonShift,
-        subBoxes.latBase, subBoxes.lonBase,
-        subBoxes.maxLatDelta, subBoxes.maxLonDelta,
-        subBoxes.relations,
-        tree);
+            subBoxes.latShift,
+            subBoxes.lonShift,
+            subBoxes.latBase,
+            subBoxes.lonBase,
+            subBoxes.maxLatDelta,
+            subBoxes.maxLonDelta,
+            subBoxes.relations,
+            tree);
   }
 
-  private static Grid createSubBoxes(Rectangle boundingBox, Function<Rectangle, Relation> boxToRelation) {
-    final int minLat = encodeLatitudeCeil(boundingBox.minLat);
-    final int maxLat = encodeLatitude(boundingBox.maxLat);
-    final int minLon = encodeLongitudeCeil(boundingBox.minLon);
-    final int maxLon = encodeLongitude(boundingBox.maxLon);
-
-    if (maxLat < minLat || (boundingBox.crossesDateline() == false && maxLon < minLon)) {
+  private static Grid createSubBoxes(
+          double shapeMinLat,
+          double shapeMaxLat,
+          double shapeMinLon,
+          double shapeMaxLon,
+          Function<Rectangle, Relation> boxToRelation) {
+    final int minLat = encodeLatitudeCeil(shapeMinLat);
+    final int maxLat = encodeLatitude(shapeMaxLat);
+    final int minLon = encodeLongitudeCeil(shapeMinLon);
+    final int maxLon = encodeLongitude(shapeMaxLon);
+
+    if (maxLat < minLat || (shapeMaxLon >= shapeMinLon && maxLon < minLon)) {
       // the box cannot match any quantized point
       return new Grid(1, 1, 0, 0, 0, 0, new byte[0]);
     }
@@ -220,7 +253,7 @@ public final class GeoEncodingUtils {
     {
       long minLon2 = (long) minLon - Integer.MIN_VALUE;
       long maxLon2 = (long) maxLon - Integer.MIN_VALUE;
-      if (boundingBox.crossesDateline()) {
+      if (shapeMaxLon < shapeMinLon) { // crosses dateline
         maxLon2 += 1L << 32; // wrap
       }
       lonShift = computeShift(minLon2, maxLon2);
@@ -237,22 +270,24 @@ public final class GeoEncodingUtils {
         final int boxMaxLat = boxMinLat + (1 << latShift) - 1;
         final int boxMaxLon = boxMinLon + (1 << lonShift) - 1;
 
-        relations[i * maxLonDelta + j] = (byte) boxToRelation.apply(new Rectangle(
-                decodeLatitude(boxMinLat), decodeLatitude(boxMaxLat),
-            decodeLongitude(boxMinLon), decodeLongitude(boxMaxLon))
-            ).ordinal();
+        relations[i * maxLonDelta + j] =
+                (byte)
+                        boxToRelation
+                                .apply(
+                                        new Rectangle(
+                                                decodeLatitude(boxMinLat), decodeLatitude(boxMaxLat),
+                                                decodeLongitude(boxMinLon), decodeLongitude(boxMaxLon)))
+                                .ordinal();
       }
     }
 
-    return new Grid(
-        latShift, lonShift,
-        latBase, lonBase,
-        maxLatDelta, maxLonDelta,
-        relations);
+    return new Grid(latShift, lonShift, latBase, lonBase, maxLatDelta, maxLonDelta, relations);
   }
 
-  /** Compute the minimum shift value so that
-   * {@code (b>>>shift)-(a>>>shift)} is less that {@code ARITY}. */
+  /**
+   * Compute the minimum shift value so that {@code (b>>>shift)-(a>>>shift)} is less that {@code
+   * ARITY}.
+   */
   private static int computeShift(long a, long b) {
     assert a <= b;
     // We enforce a shift of at least 1 so that when we work with unsigned ints
@@ -276,10 +311,13 @@ public final class GeoEncodingUtils {
     final byte[] relations;
 
     private Grid(
-        int latShift, int lonShift,
-        int latBase, int lonBase,
-        int maxLatDelta, int maxLonDelta,
-        byte[] relations) {
+            int latShift,
+            int lonShift,
+            int latBase,
+            int lonBase,
+            int maxLatDelta,
+            int maxLonDelta,
+            byte[] relations) {
       if (latShift < 1 || latShift > 31) {
         throw new IllegalArgumentException();
       }
@@ -303,19 +341,26 @@ public final class GeoEncodingUtils {
     private final double distanceKey;
 
     private DistancePredicate(
-        int latShift, int lonShift,
-        int latBase, int lonBase,
-        int maxLatDelta, int maxLonDelta,
-        byte[] relations,
-        double lat, double lon, double distanceKey) {
+            int latShift,
+            int lonShift,
+            int latBase,
+            int lonBase,
+            int maxLatDelta,
+            int maxLonDelta,
+            byte[] relations,
+            double lat,
+            double lon,
+            double distanceKey) {
       super(latShift, lonShift, latBase, lonBase, maxLatDelta, maxLonDelta, relations);
       this.lat = lat;
       this.lon = lon;
       this.distanceKey = distanceKey;
     }
 
-    /** Check whether the given point is within a distance of another point.
-     *  NOTE: this operates directly on the encoded representation of points. */
+    /**
+     * Check whether the given point is within a distance of another point. NOTE: this operates
+     * directly on the encoded representation of points.
+     */
     public boolean test(int lat, int lon) {
       final int lat2 = ((lat - Integer.MIN_VALUE) >>> latShift);
       if (lat2 < latBase || lat2 >= latBase + maxLatDelta) {
@@ -334,8 +379,8 @@ public final class GeoEncodingUtils {
       final int relation = relations[(lat2 - latBase) * maxLonDelta + (lon2 - lonBase)];
       if (relation == Relation.CELL_CROSSES_QUERY.ordinal()) {
         return SloppyMath.haversinSortKey(
-            decodeLatitude(lat), decodeLongitude(lon),
-            this.lat, this.lon) <= distanceKey;
+                decodeLatitude(lat), decodeLongitude(lon), this.lat, this.lon)
+                <= distanceKey;
       } else {
         return relation == Relation.CELL_INSIDE_QUERY.ordinal();
       }
@@ -348,17 +393,22 @@ public final class GeoEncodingUtils {
     private final Component2D tree;
 
     private Component2DPredicate(
-        int latShift, int lonShift,
-        int latBase, int lonBase,
-        int maxLatDelta, int maxLonDelta,
-        byte[] relations,
-        Component2D tree) {
+            int latShift,
+            int lonShift,
+            int latBase,
+            int lonBase,
+            int maxLatDelta,
+            int maxLonDelta,
+            byte[] relations,
+            Component2D tree) {
       super(latShift, lonShift, latBase, lonBase, maxLatDelta, maxLonDelta, relations);
       this.tree = tree;
     }
 
-    /** Check whether the given point is within the considered polygon.
-     *  NOTE: this operates directly on the encoded representation of points. */
+    /**
+     * Check whether the given point is within the considered polygon. NOTE: this operates directly
+     * on the encoded representation of points.
+     */
     public boolean test(int lat, int lon) {
       final int lat2 = ((lat - Integer.MIN_VALUE) >>> latShift);
       if (lat2 < latBase || lat2 >= latBase + maxLatDelta) {
diff --git a/lucene/core/src/java/org/apache/lucene/geo/Point2D.java b/lucene/core/src/java/org/apache/lucene/geo/Point2D.java
index 3757f0a..9d384d9 100644
--- a/lucene/core/src/java/org/apache/lucene/geo/Point2D.java
+++ b/lucene/core/src/java/org/apache/lucene/geo/Point2D.java
@@ -17,17 +17,15 @@
 
 package org.apache.lucene.geo;
 
-import org.apache.lucene.index.PointValues;
-
 import static org.apache.lucene.geo.GeoUtils.orient;
 
-/**
- * 2D point implementation containing geo spatial logic.
- */
+import org.apache.lucene.index.PointValues;
+
+/** 2D point implementation containing geo spatial logic. */
 final class Point2D implements Component2D {
 
-  final private double x;
-  final private double y;
+  private final double x;
+  private final double y;
 
   private Point2D(double x, double y) {
     this.x = x;
@@ -68,27 +66,59 @@ final class Point2D implements Component2D {
   }
 
   @Override
-  public boolean intersectsLine(double minX, double maxX, double minY, double maxY,
-                                double aX, double aY, double bX, double bY) {
-    return Component2D.containsPoint(x, y, minX, maxX, minY, maxY) &&
-           orient(aX, aY, bX, bY, x, y) == 0;
+  public boolean intersectsLine(
+          double minX,
+          double maxX,
+          double minY,
+          double maxY,
+          double aX,
+          double aY,
+          double bX,
+          double bY) {
+    return Component2D.containsPoint(x, y, minX, maxX, minY, maxY)
+            && orient(aX, aY, bX, bY, x, y) == 0;
   }
 
   @Override
-  public boolean intersectsTriangle(double minX, double maxX, double minY, double maxY,
-                                    double aX, double aY, double bX, double bY, double cX, double cY) {
+  public boolean intersectsTriangle(
+          double minX,
+          double maxX,
+          double minY,
+          double maxY,
+          double aX,
+          double aY,
+          double bX,
+          double bY,
+          double cX,
+          double cY) {
     return Component2D.pointInTriangle(minX, maxX, minY, maxY, x, y, aX, aY, bX, bY, cX, cY);
   }
 
   @Override
-  public boolean containsLine(double minX, double maxX, double minY, double maxY,
-                              double aX, double aY, double bX, double bY) {
+  public boolean containsLine(
+          double minX,
+          double maxX,
+          double minY,
+          double maxY,
+          double aX,
+          double aY,
+          double bX,
+          double bY) {
     return 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) {
+  public boolean containsTriangle(
+          double minX,
+          double maxX,
+          double minY,
+          double maxY,
+          double aX,
+          double aY,
+          double bX,
+          double bY,
+          double cX,
+          double cY) {
     return false;
   }
 
@@ -98,15 +128,37 @@ final class Point2D implements Component2D {
   }
 
   @Override
-  public WithinRelation withinLine(double minX, double maxX, double minY, double maxY,
-                                   double aX, double aY, boolean ab, double bX, double bY) {
+  public WithinRelation withinLine(
+          double minX,
+          double maxX,
+          double minY,
+          double maxY,
+          double aX,
+          double aY,
+          boolean ab,
+          double bX,
+          double bY) {
     // can be improved?
-    return intersectsLine(minX, maxX, minY, maxY, aX, aY, bX, bY) ? WithinRelation.CANDIDATE : WithinRelation.DISJOINT;
+    return intersectsLine(minX, maxX, minY, maxY, aX, aY, bX, bY)
+            ? WithinRelation.CANDIDATE
+            : 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) {
+  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.pointInTriangle(minX, maxX, minY, maxY, x, y, aX, aY, bX, bY, cX, cY)) {
       return WithinRelation.CANDIDATE;
     }
@@ -115,8 +167,17 @@ final class Point2D implements Component2D {
 
   /** create a Point2D component tree from a LatLon point */
   static Component2D create(Point point) {
-    return new Point2D(GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(point.getLon())),
-        GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(point.getLat())));
+    // Points behave as rectangles
+    double qLat =
+            point.getLat() == GeoUtils.MAX_LAT_INCL
+                    ? point.getLat()
+                    : GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitudeCeil(point.getLat()));
+    double qLon =
+            point.getLon() == GeoUtils.MAX_LON_INCL
+                    ? point.getLon()
+                    : GeoEncodingUtils.decodeLongitude(
+                    GeoEncodingUtils.encodeLongitudeCeil(point.getLon()));
+    return new Point2D(qLon, qLat);
   }
 
   /** create a Point2D component tree from a XY point */
diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseLatLonDocValueTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonDocValueTestCase.java
new file mode 100644
index 0000000..2e96c3a
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonDocValueTestCase.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.document;
+
+import java.util.Arrays;
+import org.apache.lucene.document.ShapeField.QueryRelation;
+import org.apache.lucene.geo.Circle;
+import org.apache.lucene.geo.Line;
+import org.apache.lucene.geo.Point;
+import org.apache.lucene.geo.Polygon;
+import org.apache.lucene.geo.Rectangle;
+import org.apache.lucene.search.Query;
+
+/**
+ * Base test case for testing geospatial indexing and search functionality for {@link
+ * LatLonDocValuesField} *
+ */
+public abstract class BaseLatLonDocValueTestCase extends BaseLatLonSpatialTestCase {
+
+  @Override
+  protected Query newRectQuery(
+      String field,
+      QueryRelation queryRelation,
+      double minLon,
+      double maxLon,
+      double minLat,
+      double maxLat) {
+    return LatLonDocValuesField.newSlowGeometryQuery(
+        field, queryRelation, new Rectangle(minLat, maxLat, minLon, maxLon));
+  }
+
+  @Override
+  protected Query newLineQuery(String field, QueryRelation queryRelation, Object... lines) {
+    return LatLonDocValuesField.newSlowGeometryQuery(
+        field, queryRelation, Arrays.stream(lines).toArray(Line[]::new));
+  }
+
+  @Override
+  protected Query newPolygonQuery(String field, QueryRelation queryRelation, Object... polygons) {
+    return LatLonDocValuesField.newSlowGeometryQuery(
+        field, queryRelation, Arrays.stream(polygons).toArray(Polygon[]::new));
+  }
+
+  @Override
+  protected Query newDistanceQuery(String field, QueryRelation queryRelation, Object circle) {
+    return LatLonDocValuesField.newSlowGeometryQuery(field, queryRelation, (Circle) circle);
+  }
+
+  @Override
+  protected Query newPointsQuery(String field, QueryRelation queryRelation, Object... points) {
+    Point[] pointsArray = new Point[points.length];
+    for (int i = 0; i < points.length; i++) {
+      double[] point = (double[]) points[i];
+      pointsArray[i] = new Point(point[0], point[1]);
+    }
+    return LatLonDocValuesField.newSlowGeometryQuery(field, queryRelation, pointsArray);
+  }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseLatLonPointTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonPointTestCase.java
new file mode 100644
index 0000000..d886979
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonPointTestCase.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.document;
+
+import com.carrotsearch.randomizedtesting.generators.RandomPicks;
+import java.util.Arrays;
+import org.apache.lucene.document.ShapeField.QueryRelation;
+import org.apache.lucene.geo.Circle;
+import org.apache.lucene.geo.GeoTestUtil;
+import org.apache.lucene.geo.Line;
+import org.apache.lucene.geo.Point;
+import org.apache.lucene.geo.Polygon;
+import org.apache.lucene.geo.Rectangle;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryUtils;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.IOUtils;
+
+/**
+ * Base test case for testing geospatial indexing and search functionality for {@link LatLonPoint} *
+ */
+public abstract class BaseLatLonPointTestCase extends BaseLatLonSpatialTestCase {
+
+  @Override
+  protected Query newRectQuery(
+      String field,
+      QueryRelation queryRelation,
+      double minLon,
+      double maxLon,
+      double minLat,
+      double maxLat) {
+    return LatLonPoint.newGeometryQuery(
+        field, queryRelation, new Rectangle(minLat, maxLat, minLon, maxLon));
+  }
+
+  @Override
+  protected Query newLineQuery(String field, QueryRelation queryRelation, Object... lines) {
+    return LatLonPoint.newGeometryQuery(
+        field, queryRelation, Arrays.stream(lines).toArray(Line[]::new));
+  }
+
+  @Override
+  protected Query newPolygonQuery(String field, QueryRelation queryRelation, Object... polygons) {
+    return LatLonPoint.newGeometryQuery(
+        field, queryRelation, Arrays.stream(polygons).toArray(Polygon[]::new));
+  }
+
+  @Override
+  protected Query newDistanceQuery(String field, QueryRelation queryRelation, Object circle) {
+    return LatLonPoint.newGeometryQuery(field, queryRelation, (Circle) circle);
+  }
+
+  @Override
+  protected Query newPointsQuery(String field, QueryRelation queryRelation, Object... points) {
+    Point[] pointsArray = new Point[points.length];
+    for (int i = 0; i < points.length; i++) {
+      double[] point = (double[]) points[i];
+      pointsArray[i] = new Point(point[0], point[1]);
+    }
+    return LatLonPoint.newGeometryQuery(field, queryRelation, pointsArray);
+  }
+
+  public void testBoundingBoxQueriesEquivalence() throws Exception {
+    int numShapes = atLeast(20);
+
+    Directory dir = newDirectory();
+    RandomIndexWriter w = new RandomIndexWriter(random(), dir);
+
+    for (int i = 0; i < numShapes; i++) {
+      indexRandomShapes(w.w, nextShape());
+    }
+    if (random().nextBoolean()) {
+      w.forceMerge(1);
+    }
+
+    ///// search //////
+    IndexReader reader = w.getReader();
+    w.close();
+    IndexSearcher searcher = newSearcher(reader);
+
+    Rectangle box = GeoTestUtil.nextBox();
+
+    Query q1 = LatLonPoint.newBoxQuery(FIELD_NAME, box.minLat, box.maxLat, box.minLon, box.maxLon);
+    Query q2 = new LatLonPointQuery(FIELD_NAME, QueryRelation.INTERSECTS, box);
+    assertEquals(searcher.count(q1), searcher.count(q2));
+
+    IOUtils.close(w, reader, dir);
+  }
+
+  public void testQueryEqualsAndHashcode() {
+    Polygon polygon = GeoTestUtil.nextPolygon();
+    QueryRelation queryRelation =
+        RandomPicks.randomFrom(
+            random(), new QueryRelation[] {QueryRelation.INTERSECTS, QueryRelation.DISJOINT});
+    String fieldName = "foo";
+    Query q1 = newPolygonQuery(fieldName, queryRelation, polygon);
+    Query q2 = newPolygonQuery(fieldName, queryRelation, polygon);
+    QueryUtils.checkEqual(q1, q2);
+    // different field name
+    Query q3 = newPolygonQuery("bar", queryRelation, polygon);
+    QueryUtils.checkUnequal(q1, q3);
+    // different query relation
+    QueryRelation newQueryRelation =
+        RandomPicks.randomFrom(
+            random(), new QueryRelation[] {QueryRelation.INTERSECTS, QueryRelation.DISJOINT});
+    Query q4 = newPolygonQuery(fieldName, newQueryRelation, polygon);
+    if (queryRelation == newQueryRelation) {
+      QueryUtils.checkEqual(q1, q4);
+    } else {
+      QueryUtils.checkUnequal(q1, q4);
+    }
+    // different shape
+    Polygon newPolygon = GeoTestUtil.nextPolygon();
+    ;
+    Query q5 = newPolygonQuery(fieldName, queryRelation, newPolygon);
+    if (polygon.equals(newPolygon)) {
+      QueryUtils.checkEqual(q1, q5);
+    } else {
+      QueryUtils.checkUnequal(q1, q5);
+    }
+  }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java
index 4277d61..ad71f67 100644
--- a/lucene/core/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java
+++ b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java
@@ -16,19 +16,14 @@
  */
 package org.apache.lucene.document;
 
-import java.util.Arrays;
-
 import com.carrotsearch.randomizedtesting.generators.RandomPicks;
+import java.util.Arrays;
 import org.apache.lucene.document.ShapeField.QueryRelation;
-import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.Circle;
 import org.apache.lucene.geo.GeoTestUtil;
-import org.apache.lucene.geo.GeoUtils;
-import org.apache.lucene.geo.LatLonGeometry;
 import org.apache.lucene.geo.Line;
-import org.apache.lucene.geo.Point;
 import org.apache.lucene.geo.Polygon;
 import org.apache.lucene.geo.Rectangle;
-import org.apache.lucene.geo.Tessellator;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.RandomIndexWriter;
 import org.apache.lucene.search.IndexSearcher;
@@ -36,73 +31,39 @@ import org.apache.lucene.search.Query;
 import org.apache.lucene.search.QueryUtils;
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.util.IOUtils;
-import org.apache.lucene.util.TestUtil;
-import org.apache.lucene.geo.Circle;
-
-import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitudeCeil;
-import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
-import static org.apache.lucene.geo.GeoTestUtil.nextLatitude;
-import static org.apache.lucene.geo.GeoTestUtil.nextLongitude;
-
-/** Base test case for testing geospatial indexing and search functionality **/
-public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase {
-
-  protected abstract ShapeType getShapeType();
 
-  protected Object nextShape() {
-    return getShapeType().nextShape();
-  }
+/**
+ * Base test case for testing geospatial indexing and search functionality for {@link LatLonShape} *
+ */
+public abstract class BaseLatLonShapeTestCase extends BaseLatLonSpatialTestCase {
 
-  /** factory method to create a new bounding box query */
   @Override
-  protected Query newRectQuery(String field, QueryRelation queryRelation, double minLon, double maxLon, double minLat, double maxLat) {
+  protected Query newRectQuery(
+          String field,
+          QueryRelation queryRelation,
+          double minLon,
+          double maxLon,
+          double minLat,
+          double maxLat) {
     return LatLonShape.newBoxQuery(field, queryRelation, minLat, maxLat, minLon, maxLon);
   }
 
-  /** factory method to create a new line query */
   @Override
   protected Query newLineQuery(String field, QueryRelation queryRelation, Object... lines) {
-    return LatLonShape.newLineQuery(field, queryRelation, Arrays.stream(lines).toArray(Line[]::new));
+    return LatLonShape.newLineQuery(
+            field, queryRelation, Arrays.stream(lines).toArray(Line[]::new));
   }
 
-  /** factory method to create a new polygon query */
   @Override
   protected Query newPolygonQuery(String field, QueryRelation queryRelation, Object... polygons) {
-    return LatLonShape.newPolygonQuery(field, queryRelation, Arrays.stream(polygons).toArray(Polygon[]::new));
+    return LatLonShape.newPolygonQuery(
+            field, queryRelation, Arrays.stream(polygons).toArray(Polygon[]::new));
   }
 
   @Override
   protected Query newPointsQuery(String field, QueryRelation queryRelation, Object... points) {
-    return LatLonShape.newPointQuery(field, queryRelation, Arrays.stream(points).toArray(double[][]::new));
-  }
-
-  @Override
-  protected Component2D toLine2D(Object... lines) {
-    return LatLonGeometry.create(Arrays.stream(lines).toArray(Line[]::new));
-  }
-
-  @Override
-  protected Component2D toPolygon2D(Object... polygons) {
-    return LatLonGeometry.create(Arrays.stream(polygons).toArray(Polygon[]::new));
-  }
-
-  @Override
-  protected Component2D toRectangle2D(double minX, double maxX, double minY, double maxY) {
-    return LatLonGeometry.create(new Rectangle(minY, maxY, minX, maxX));
-  }
-
-  @Override
-  protected Component2D toPoint2D(Object... points) {
-    double[][] p = Arrays.stream(points).toArray(double[][]::new);
-    org.apache.lucene.geo.Point[] pointArray = new org.apache.lucene.geo.Point[points.length];
-    for (int i =0; i < points.length; i++) {
-      pointArray[i] = new org.apache.lucene.geo.Point(p[i][0], p[i][1]);
-    }
-    return LatLonGeometry.create(pointArray);
+    return LatLonShape.newPointQuery(
+            field, queryRelation, Arrays.stream(points).toArray(double[][]::new));
   }
 
   @Override
@@ -110,69 +71,110 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase {
     return LatLonShape.newDistanceQuery(field, queryRelation, (Circle) circle);
   }
 
-  @Override
-  protected Component2D toCircle2D(Object circle) {
-    return LatLonGeometry.create((Circle) circle);
-  }
-
-  @Override
-  protected Circle nextCircle() {
-    final double radiusMeters = random().nextDouble() * GeoUtils.EARTH_MEAN_RADIUS_METERS * Math.PI / 2.0 + 1.0;
-    return new Circle(nextLatitude(), nextLongitude(), radiusMeters);
-  }
+  public void testBoundingBoxQueriesEquivalence() throws Exception {
+    int numShapes = atLeast(20);
 
-  @Override
-  public Rectangle randomQueryBox() {
-    return GeoTestUtil.nextBox();
-  }
+    Directory dir = newDirectory();
+    RandomIndexWriter w = new RandomIndexWriter(random(), dir);
 
-  @Override
-  protected Object[] nextPoints() {
-    int numPoints = TestUtil.nextInt(random(), 1, 20);
-    double[][] points = new double[numPoints][2];
-    for (int i = 0; i < numPoints; i++) {
-      points[i][0] = nextLatitude();
-      points[i][1] = nextLongitude();
+    for (int i = 0; i < numShapes; i++) {
+      indexRandomShapes(w.w, nextShape());
+    }
+    if (random().nextBoolean()) {
+      w.forceMerge(1);
     }
-    return points;
-  }
 
-  @Override
-  protected double rectMinX(Object rect) {
-    return ((Rectangle)rect).minLon;
-  }
+    ///// search //////
+    IndexReader reader = w.getReader();
+    w.close();
+    IndexSearcher searcher = newSearcher(reader);
 
-  @Override
-  protected double rectMaxX(Object rect) {
-    return ((Rectangle)rect).maxLon;
-  }
+    Rectangle box = GeoTestUtil.nextBox();
 
-  @Override
-  protected double rectMinY(Object rect) {
-    return ((Rectangle)rect).minLat;
+    Query q1 =
+            LatLonShape.newBoxQuery(
+                    FIELD_NAME, QueryRelation.INTERSECTS, box.minLat, box.maxLat, box.minLon, box.maxLon);
+    Query q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.INTERSECTS, box);
+    assertEquals(searcher.count(q1), searcher.count(q2));
+    q1 =
+            LatLonShape.newBoxQuery(
+                    FIELD_NAME, QueryRelation.WITHIN, box.minLat, box.maxLat, box.minLon, box.maxLon);
+    q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.WITHIN, box);
+    assertEquals(searcher.count(q1), searcher.count(q2));
+    q1 =
+            LatLonShape.newBoxQuery(
+                    FIELD_NAME, QueryRelation.CONTAINS, box.minLat, box.maxLat, box.minLon, box.maxLon);
+    if (box.crossesDateline()) {
+      q2 = LatLonShape.newGeometryQuery(FIELD_NAME, QueryRelation.CONTAINS, box);
+    } else {
+      q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.CONTAINS, box);
+    }
+    assertEquals(searcher.count(q1), searcher.count(q2));
+    q1 =
+            LatLonShape.newBoxQuery(
+                    FIELD_NAME, QueryRelation.DISJOINT, box.minLat, box.maxLat, box.minLon, box.maxLon);
+    q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.DISJOINT, box);
+    assertEquals(searcher.count(q1), searcher.count(q2));
+
+    IOUtils.close(w, reader, dir);
   }
 
   public void testBoxQueryEqualsAndHashcode() {
     Rectangle rectangle = GeoTestUtil.nextBox();
     QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
     String fieldName = "foo";
-    Query q1 = newRectQuery(fieldName, queryRelation, rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat);
-    Query q2 = newRectQuery(fieldName, queryRelation, rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat);
+    Query q1 =
+            newRectQuery(
+                    fieldName,
+                    queryRelation,
+                    rectangle.minLon,
+                    rectangle.maxLon,
+                    rectangle.minLat,
+                    rectangle.maxLat);
+    Query q2 =
+            newRectQuery(
+                    fieldName,
+                    queryRelation,
+                    rectangle.minLon,
+                    rectangle.maxLon,
+                    rectangle.minLat,
+                    rectangle.maxLat);
     QueryUtils.checkEqual(q1, q2);
-    //different field name
-    Query q3 = newRectQuery("bar", queryRelation, rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat);
+    // different field name
+    Query q3 =
+            newRectQuery(
+                    "bar",
+                    queryRelation,
+                    rectangle.minLon,
+                    rectangle.maxLon,
+                    rectangle.minLat,
+                    rectangle.maxLat);
     QueryUtils.checkUnequal(q1, q3);
-    //different query relation
+    // different query relation
     QueryRelation newQueryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
-    Query q4 = newRectQuery(fieldName, newQueryRelation, rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat);
+    Query q4 =
+            newRectQuery(
+                    fieldName,
+                    newQueryRelation,
+                    rectangle.minLon,
+                    rectangle.maxLon,
+                    rectangle.minLat,
+                    rectangle.maxLat);
     if (queryRelation == newQueryRelation) {
       QueryUtils.checkEqual(q1, q4);
     } else {
       QueryUtils.checkUnequal(q1, q4);
     }
-    //different shape
+    // different shape
     Rectangle newRectangle = GeoTestUtil.nextBox();
-    Query q5 = newRectQuery(fieldName, queryRelation, newRectangle.minLon, newRectangle.maxLon, newRectangle.minLat, newRectangle.maxLat);
+    Query q5 =
+            newRectQuery(
+                    fieldName,
+                    queryRelation,
+                    newRectangle.minLon,
+                    newRectangle.maxLon,
+                    newRectangle.minLat,
+                    newRectangle.maxLat);
     if (rectangle.equals(newRectangle)) {
       QueryUtils.checkEqual(q1, q5);
     } else {
@@ -180,11 +182,6 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase {
     }
   }
 
-  /** factory method to create a new line query */
-  protected Query newLineQuery(String field, QueryRelation queryRelation, Line... lines) {
-    return LatLonShape.newLineQuery(field, queryRelation, lines);
-  }
-
   public void testLineQueryEqualsAndHashcode() {
     Line line = nextLine();
     QueryRelation queryRelation = RandomPicks.randomFrom(random(), POINT_LINE_RELATIONS);
@@ -192,10 +189,10 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase {
     Query q1 = newLineQuery(fieldName, queryRelation, line);
     Query q2 = newLineQuery(fieldName, queryRelation, line);
     QueryUtils.checkEqual(q1, q2);
-    //different field name
+    // different field name
     Query q3 = newLineQuery("bar", queryRelation, line);
     QueryUtils.checkUnequal(q1, q3);
-    //different query relation
+    // different query relation
     QueryRelation newQueryRelation = RandomPicks.randomFrom(random(), POINT_LINE_RELATIONS);
     Query q4 = newLineQuery(fieldName, newQueryRelation, line);
     if (queryRelation == newQueryRelation) {
@@ -203,7 +200,7 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase {
     } else {
       QueryUtils.checkUnequal(q1, q4);
     }
-    //different shape
+    // different shape
     Line newLine = nextLine();
     Query q5 = newLineQuery(fieldName, queryRelation, newLine);
     if (line.equals(newLine)) {
@@ -213,51 +210,6 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase {
     }
   }
 
-  public void testBoundingBoxQueriesEquivalence() throws Exception {
-    int numShapes = atLeast(20);
-
-    Directory dir = newDirectory();
-    RandomIndexWriter w = new RandomIndexWriter(random(), dir);
-
-    for (int  i =0; i < numShapes; i++) {
-      indexRandomShapes(w.w, nextShape());
-    }
-    if (random().nextBoolean()) {
-      w.forceMerge(1);
-    }
-
-    ///// search //////
-    IndexReader reader = w.getReader();
-    w.close();
-    IndexSearcher searcher = newSearcher(reader);
-
-    Rectangle box = GeoTestUtil.nextBox();
-
-    Query q1 = LatLonShape.newBoxQuery(FIELD_NAME, QueryRelation.INTERSECTS, box.minLat, box.maxLat, box.minLon, box.maxLon);
-    Query q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.INTERSECTS, box);
-    assertEquals(searcher.count(q1), searcher.count(q2));
-    q1 = LatLonShape.newBoxQuery(FIELD_NAME, QueryRelation.WITHIN, box.minLat, box.maxLat, box.minLon, box.maxLon);
-    q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.WITHIN, box);
-    assertEquals(searcher.count(q1), searcher.count(q2));
-    q1 = LatLonShape.newBoxQuery(FIELD_NAME, QueryRelation.CONTAINS, box.minLat, box.maxLat, box.minLon, box.maxLon);
-    if (box.crossesDateline()) {
-      q2 = LatLonShape.newGeometryQuery(FIELD_NAME, QueryRelation.CONTAINS, box);
-    } else {
-      q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.CONTAINS, box);
-    }
-    assertEquals(searcher.count(q1), searcher.count(q2));
-    q1 = LatLonShape.newBoxQuery(FIELD_NAME, QueryRelation.DISJOINT, box.minLat, box.maxLat, box.minLon, box.maxLon);
-    q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.DISJOINT, box);
-    assertEquals(searcher.count(q1), searcher.count(q2));
-
-    IOUtils.close(w, reader, dir);
-  }
-
-  /** factory method to create a new polygon query */
-  protected Query newPolygonQuery(String field, QueryRelation queryRelation, Polygon... polygons) {
-    return LatLonShape.newPolygonQuery(field, queryRelation, polygons);
-  }
-
   public void testPolygonQueryEqualsAndHashcode() {
     Polygon polygon = GeoTestUtil.nextPolygon();
     QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
@@ -265,10 +217,10 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase {
     Query q1 = newPolygonQuery(fieldName, queryRelation, polygon);
     Query q2 = newPolygonQuery(fieldName, queryRelation, polygon);
     QueryUtils.checkEqual(q1, q2);
-    //different field name
+    // different field name
     Query q3 = newPolygonQuery("bar", queryRelation, polygon);
     QueryUtils.checkUnequal(q1, q3);
-    //different query relation
+    // different query relation
     QueryRelation newQueryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
     Query q4 = newPolygonQuery(fieldName, newQueryRelation, polygon);
     if (queryRelation == newQueryRelation) {
@@ -276,8 +228,9 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase {
     } else {
       QueryUtils.checkUnequal(q1, q4);
     }
-    //different shape
-    Polygon newPolygon = GeoTestUtil.nextPolygon();;
+    // different shape
+    Polygon newPolygon = GeoTestUtil.nextPolygon();
+    ;
     Query q5 = newPolygonQuery(fieldName, queryRelation, newPolygon);
     if (polygon.equals(newPolygon)) {
       QueryUtils.checkEqual(q1, q5);
@@ -285,98 +238,4 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase {
       QueryUtils.checkUnequal(q1, q5);
     }
   }
-
-  @Override
-  protected double rectMaxY(Object rect) {
-    return ((Rectangle)rect).maxLat;
-  }
-
-  @Override
-  protected boolean rectCrossesDateline(Object rect) {
-    return ((Rectangle)rect).crossesDateline();
-  }
-  
-  @Override
-  public Line nextLine() {
-    return GeoTestUtil.nextLine();
-  }
-
-  @Override
-  protected Polygon nextPolygon() {
-    return GeoTestUtil.nextPolygon();
-  }
-
-  @Override
-  protected Encoder getEncoder() {
-    return new Encoder() {
-      @Override
-      double decodeX(int encoded) {
-        return decodeLongitude(encoded);
-      }
-
-      @Override
-      double decodeY(int encoded) {
-        return decodeLatitude(encoded);
-      }
-
-      @Override
-      double quantizeX(double raw) {
-        return decodeLongitude(encodeLongitude(raw));
-      }
-
-      @Override
-      double quantizeXCeil(double raw) {
-        return decodeLongitude(encodeLongitudeCeil(raw));
-      }
-
-      @Override
-      double quantizeY(double raw) {
-        return decodeLatitude(encodeLatitude(raw));
-      }
-
-      @Override
-      double quantizeYCeil(double raw) {
-        return decodeLatitude(encodeLatitudeCeil(raw));
-      }
-    };
-  }
-
-  /** internal shape type for testing different shape types */
-  protected enum ShapeType {
-    POINT() {
-      public Point nextShape() {
-        return GeoTestUtil.nextPoint();
-      }
-    },
-    LINE() {
-      public Line nextShape() {
-        return GeoTestUtil.nextLine();
-      }
-    },
-    POLYGON() {
-      public Polygon nextShape() {
-        while (true) {
-          Polygon p =  GeoTestUtil.nextPolygon();
-          try {
-            Tessellator.tessellate(p);
-            return p;
-          } catch (IllegalArgumentException e) {
-            // if we can't tessellate; then random polygon generator created a malformed shape
-          }
-        }
-      }
-    },
-    MIXED() {
-      public Object nextShape() {
-        return RandomPicks.randomFrom(random(), subList).nextShape();
-      }
-    };
-
-    static ShapeType[] subList;
-    static {
-      subList = new ShapeType[] {POINT, LINE, POLYGON};
-    }
-
-    public abstract Object nextShape();
-  }
 }
diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseLatLonSpatialTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonSpatialTestCase.java
new file mode 100644
index 0000000..518c9be
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonSpatialTestCase.java
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.document;
+
+import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
+import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
+import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
+import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitudeCeil;
+import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
+import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
+import static org.apache.lucene.geo.GeoTestUtil.nextLatitude;
+import static org.apache.lucene.geo.GeoTestUtil.nextLongitude;
+
+import com.carrotsearch.randomizedtesting.generators.RandomPicks;
+import java.util.Arrays;
+import org.apache.lucene.document.ShapeField.QueryRelation;
+import org.apache.lucene.geo.Circle;
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.GeoTestUtil;
+import org.apache.lucene.geo.GeoUtils;
+import org.apache.lucene.geo.LatLonGeometry;
+import org.apache.lucene.geo.Line;
+import org.apache.lucene.geo.Point;
+import org.apache.lucene.geo.Polygon;
+import org.apache.lucene.geo.Rectangle;
+import org.apache.lucene.geo.Tessellator;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.util.TestUtil;
+
+/** Base test case for testing geospatial indexing and search functionality * */
+public abstract class BaseLatLonSpatialTestCase extends BaseSpatialTestCase {
+
+  protected abstract ShapeType getShapeType();
+
+  protected Object nextShape() {
+    return getShapeType().nextShape();
+  }
+
+  @Override
+  protected Component2D toLine2D(Object... lines) {
+    return LatLonGeometry.create(Arrays.stream(lines).toArray(Line[]::new));
+  }
+
+  @Override
+  protected Component2D toPolygon2D(Object... polygons) {
+    return LatLonGeometry.create(Arrays.stream(polygons).toArray(Polygon[]::new));
+  }
+
+  @Override
+  protected Component2D toRectangle2D(double minX, double maxX, double minY, double maxY) {
+    return LatLonGeometry.create(new Rectangle(minY, maxY, minX, maxX));
+  }
+
+  @Override
+  protected Component2D toPoint2D(Object... points) {
+    double[][] p = Arrays.stream(points).toArray(double[][]::new);
+    Point[] pointArray = new Point[points.length];
+    for (int i = 0; i < points.length; i++) {
+      pointArray[i] = new Point(p[i][0], p[i][1]);
+    }
+    return LatLonGeometry.create(pointArray);
+  }
+
+  @Override
+  protected Component2D toCircle2D(Object circle) {
+    return LatLonGeometry.create((Circle) circle);
+  }
+
+  @Override
+  protected Circle nextCircle() {
+    final double radiusMeters =
+        random().nextDouble() * GeoUtils.EARTH_MEAN_RADIUS_METERS * Math.PI / 2.0 + 1.0;
+    return new Circle(nextLatitude(), nextLongitude(), radiusMeters);
+  }
+
+  @Override
+  public Rectangle randomQueryBox() {
+    return GeoTestUtil.nextBox();
+  }
+
+  @Override
+  protected Object[] nextPoints() {
+    int numPoints = TestUtil.nextInt(random(), 1, 20);
+    double[][] points = new double[numPoints][2];
+    for (int i = 0; i < numPoints; i++) {
+      points[i][0] = nextLatitude();
+      points[i][1] = nextLongitude();
+    }
+    return points;
+  }
+
+  @Override
+  protected double rectMinX(Object rect) {
+    return ((Rectangle) rect).minLon;
+  }
+
+  @Override
+  protected double rectMaxX(Object rect) {
+    return ((Rectangle) rect).maxLon;
+  }
+
+  @Override
+  protected double rectMinY(Object rect) {
+    return ((Rectangle) rect).minLat;
+  }
+
+  /** factory method to create a new polygon query */
+  protected Query newPolygonQuery(String field, QueryRelation queryRelation, Polygon... polygons) {
+    return LatLonShape.newPolygonQuery(field, queryRelation, polygons);
+  }
+
+  @Override
+  protected double rectMaxY(Object rect) {
+    return ((Rectangle) rect).maxLat;
+  }
+
+  @Override
+  protected boolean rectCrossesDateline(Object rect) {
+    return ((Rectangle) rect).crossesDateline();
+  }
+
+  @Override
+  public Line nextLine() {
+    return GeoTestUtil.nextLine();
+  }
+
+  @Override
+  protected Polygon nextPolygon() {
+    return GeoTestUtil.nextPolygon();
+  }
+
+  @Override
+  protected Encoder getEncoder() {
+    return new Encoder() {
+      @Override
+      double decodeX(int encoded) {
+        return decodeLongitude(encoded);
+      }
+
+      @Override
+      double decodeY(int encoded) {
+        return decodeLatitude(encoded);
+      }
+
+      @Override
+      double quantizeX(double raw) {
+        return decodeLongitude(encodeLongitude(raw));
+      }
+
+      @Override
+      double quantizeXCeil(double raw) {
+        return decodeLongitude(encodeLongitudeCeil(raw));
+      }
+
+      @Override
+      double quantizeY(double raw) {
+        return decodeLatitude(encodeLatitude(raw));
+      }
+
+      @Override
+      double quantizeYCeil(double raw) {
+        return decodeLatitude(encodeLatitudeCeil(raw));
+      }
+    };
+  }
+
+  /** internal shape type for testing different shape types */
+  protected enum ShapeType {
+    POINT() {
+      public Point nextShape() {
+        return GeoTestUtil.nextPoint();
+      }
+    },
+    LINE() {
+      public Line nextShape() {
+        return GeoTestUtil.nextLine();
+      }
+    },
+    POLYGON() {
+      public Polygon nextShape() {
+        while (true) {
+          Polygon p = GeoTestUtil.nextPolygon();
+          try {
+            Tessellator.tessellate(p);
+            return p;
+          } catch (IllegalArgumentException e) {
+            // if we can't tessellate; then random polygon generator created a malformed shape
+          }
+        }
+      }
+    },
+    MIXED() {
+      public Object nextShape() {
+        return RandomPicks.randomFrom(random(), subList).nextShape();
+      }
+    };
+
+    static ShapeType[] subList;
+
+    static {
+      subList = new ShapeType[] {POINT, LINE, POLYGON};
+    }
+
+    public abstract Object nextShape();
+  }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseShapeTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseSpatialTestCase.java
similarity index 85%
rename from lucene/core/src/test/org/apache/lucene/document/BaseShapeTestCase.java
rename to lucene/core/src/test/org/apache/lucene/document/BaseSpatialTestCase.java
index 4412472..9662a2f 100644
--- a/lucene/core/src/test/org/apache/lucene/document/BaseShapeTestCase.java
+++ b/lucene/core/src/test/org/apache/lucene/document/BaseSpatialTestCase.java
@@ -16,12 +16,14 @@
  */
 package org.apache.lucene.document;
 
+import static com.carrotsearch.randomizedtesting.RandomizedTest.randomBoolean;
+import static com.carrotsearch.randomizedtesting.RandomizedTest.randomIntBetween;
+
+import com.carrotsearch.randomizedtesting.generators.RandomPicks;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
-
-import com.carrotsearch.randomizedtesting.generators.RandomPicks;
 import org.apache.lucene.document.ShapeField.QueryRelation;
 import org.apache.lucene.geo.Component2D;
 import org.apache.lucene.geo.GeoUtils;
@@ -46,24 +48,24 @@ import org.apache.lucene.util.IOUtils;
 import org.apache.lucene.util.LuceneTestCase;
 import org.apache.lucene.util.TestUtil;
 
-import static com.carrotsearch.randomizedtesting.RandomizedTest.randomBoolean;
-import static com.carrotsearch.randomizedtesting.RandomizedTest.randomIntBetween;
-
 /**
  * Base test case for testing spherical and cartesian geometry indexing and search functionality
- * <p>
- * This class is implemented by {@link BaseXYShapeTestCase} for testing XY cartesian geometry
- * and {@link BaseLatLonShapeTestCase} for testing Lat Lon geospatial geometry
- **/
-public abstract class BaseShapeTestCase extends LuceneTestCase {
+ *
+ * <p>This class is implemented by {@link BaseXYShapeTestCase} for testing XY cartesian geometry and
+ * {@link BaseLatLonSpatialTestCase} for testing Lat Lon geospatial geometry
+ */
+public abstract class BaseSpatialTestCase extends LuceneTestCase {
 
   /** name of the LatLonShape indexed field */
   protected static final String FIELD_NAME = "shape";
+
   public final Encoder ENCODER;
   public final Validator VALIDATOR;
-  protected static final QueryRelation[] POINT_LINE_RELATIONS = {QueryRelation.INTERSECTS, QueryRelation.DISJOINT, QueryRelation.CONTAINS};
+  protected static final QueryRelation[] POINT_LINE_RELATIONS = {
+          QueryRelation.INTERSECTS, QueryRelation.DISJOINT, QueryRelation.CONTAINS
+  };
 
-  public BaseShapeTestCase() {
+  public BaseSpatialTestCase() {
     ENCODER = getEncoder();
     VALIDATOR = getValidator();
   }
@@ -94,7 +96,7 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
 
     Object[] shapes = new Object[numShapes];
     for (int i = 0; i < numShapes; i++) {
-      shapes[i] =  diffShapes[random().nextInt(cardinality)];
+      shapes[i] = diffShapes[random().nextInt(cardinality)];
     }
 
     verify(shapes);
@@ -140,6 +142,7 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
   }
 
   protected abstract Object getShapeType();
+
   protected abstract Object nextShape();
 
   protected abstract Encoder getEncoder();
@@ -155,7 +158,7 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
     }
   }
 
-  /** return a semi-random line used for queries **/
+  /** return a semi-random line used for queries * */
   protected abstract Object nextLine();
 
   protected abstract Object nextPolygon();
@@ -167,16 +170,20 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
   protected abstract Object nextCircle();
 
   protected abstract double rectMinX(Object rect);
+
   protected abstract double rectMaxX(Object rect);
+
   protected abstract double rectMinY(Object rect);
+
   protected abstract double rectMaxY(Object rect);
+
   protected abstract boolean rectCrossesDateline(Object rect);
 
   /**
    * return a semi-random line used for queries
    *
-   * note: shapes parameter may be used to ensure some queries intersect indexed shapes
-   **/
+   * <p>note: shapes parameter may be used to ensure some queries intersect indexed shapes
+   */
   protected Object randomQueryLine(Object... shapes) {
     return nextLine();
   }
@@ -190,19 +197,28 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
   }
 
   /** factory method to create a new bounding box query */
-  protected abstract Query newRectQuery(String field, QueryRelation queryRelation, double minX, double maxX, double minY, double maxY);
+  protected abstract Query newRectQuery(
+          String field,
+          QueryRelation queryRelation,
+          double minX,
+          double maxX,
+          double minY,
+          double maxY);
 
   /** factory method to create a new line query */
   protected abstract Query newLineQuery(String field, QueryRelation queryRelation, Object... lines);
 
   /** factory method to create a new polygon query */
-  protected abstract Query newPolygonQuery(String field, QueryRelation queryRelation, Object... polygons);
+  protected abstract Query newPolygonQuery(
+          String field, QueryRelation queryRelation, Object... polygons);
 
   /** factory method to create a new point query */
-  protected abstract Query newPointsQuery(String field, QueryRelation queryRelation, Object... points);
+  protected abstract Query newPointsQuery(
+          String field, QueryRelation queryRelation, Object... points);
 
   /** factory method to create a new distance query */
-  protected abstract Query newDistanceQuery(String field, QueryRelation queryRelation, Object circle);
+  protected abstract Query newDistanceQuery(
+          String field, QueryRelation queryRelation, Object circle);
 
   protected abstract Component2D toLine2D(Object... line);
 
@@ -239,7 +255,6 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
     IOUtils.close(w, reader, dir);
   }
 
-
   protected void indexRandomShapes(IndexWriter w, Object... shapes) throws Exception {
     Set<Integer> deleted = new HashSet<>();
     for (int id = 0; id < shapes.length; ++id) {
@@ -252,7 +267,7 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
       w.addDocument(doc);
       if (id > 0 && random().nextInt(100) == 42) {
         int idToDelete = random().nextInt(id);
-        w.deleteDocuments(new Term("id", ""+idToDelete));
+        w.deleteDocuments(new Term("id", "" + idToDelete));
         deleted.add(idToDelete);
         if (VERBOSE) {
           System.out.println("   delete id=" + idToDelete);
@@ -289,20 +304,27 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
 
     for (int iter = 0; iter < iters; ++iter) {
       if (VERBOSE) {
-        System.out.println("\nTEST: iter=" + (iter+1) + " of " + iters + " s=" + s);
+        System.out.println("\nTEST: iter=" + (iter + 1) + " of " + iters + " s=" + s);
       }
 
       // BBox
       Object rect = randomQueryBox();
       QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
-      Query query = newRectQuery(FIELD_NAME, queryRelation, rectMinX(rect), rectMaxX(rect), rectMinY(rect), rectMaxY(rect));
+      Query query =
+              newRectQuery(
+                      FIELD_NAME,
+                      queryRelation,
+                      rectMinX(rect),
+                      rectMaxX(rect),
+                      rectMinY(rect),
+                      rectMaxY(rect));
 
       if (VERBOSE) {
         System.out.println("  query=" + query + ", relation=" + queryRelation);
       }
 
       final FixedBitSet hits = searchIndex(s, query, maxDoc);
-     
+
       boolean fail = false;
       NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
       for (int docID = 0; docID < maxDoc; ++docID) {
@@ -320,15 +342,17 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
           expected = false;
         } else {
           if (queryRelation == QueryRelation.CONTAINS && rectCrossesDateline(rect)) {
-            // For contains we need to call the validator for each section. 
+            // For contains we need to call the validator for each section.
             // It is only expected if both sides are contained.
             Component2D left = toRectangle2D(minX, GeoUtils.MAX_LON_INCL, minY, maxY);
             Component2D right = toRectangle2D(GeoUtils.MIN_LON_INCL, maxX, minY, maxY);
-            expected = VALIDATOR.setRelation(queryRelation).testComponentQuery(left, shapes[id]) &&
-                    VALIDATOR.setRelation(queryRelation).testComponentQuery(right, shapes[id]);
+            expected =
+                    VALIDATOR.setRelation(queryRelation).testComponentQuery(left, shapes[id])
+                            && VALIDATOR.setRelation(queryRelation).testComponentQuery(right, shapes[id]);
           } else {
             Component2D component2D = toRectangle2D(minX, maxX, minY, maxY);
-            expected = VALIDATOR.setRelation(queryRelation).testComponentQuery(component2D, shapes[id]);
+            expected =
+                    VALIDATOR.setRelation(queryRelation).testComponentQuery(component2D, shapes[id]);
           }
         }
 
@@ -348,7 +372,16 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
             b.append("  shape=" + shapes[id] + "\n");
           }
           b.append("  deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
-          b.append("  rect=Rectangle(lat=" + ENCODER.quantizeYCeil(rectMinY(rect)) + " TO " + ENCODER.quantizeY(rectMaxY(rect)) + " lon=" + minX + " TO " + ENCODER.quantizeX(rectMaxX(rect)) + ")\n");
+          b.append(
+                  "  rect=Rectangle(lat="
+                          + ENCODER.quantizeYCeil(rectMinY(rect))
+                          + " TO "
+                          + ENCODER.quantizeY(rectMaxY(rect))
+                          + " lon="
+                          + minX
+                          + " TO "
+                          + ENCODER.quantizeX(rectMaxX(rect))
+                          + ")\n");
           if (true) {
             fail("wrong hit (first of possibly more):\n\n" + b);
           } else {
@@ -401,7 +434,8 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
         } else if (shapes[id] == null) {
           expected = false;
         } else {
-          expected = VALIDATOR.setRelation(queryRelation).testComponentQuery(queryLine2D, shapes[id]);
+          expected =
+                  VALIDATOR.setRelation(queryRelation).testComponentQuery(queryLine2D, shapes[id]);
         }
 
         if (hits.get(docID) != expected) {
@@ -473,7 +507,8 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
         } else if (shapes[id] == null) {
           expected = false;
         } else {
-          expected = VALIDATOR.setRelation(queryRelation).testComponentQuery(queryPoly2D, shapes[id]);
+          expected =
+                  VALIDATOR.setRelation(queryRelation).testComponentQuery(queryPoly2D, shapes[id]);
         }
 
         if (hits.get(docID) != expected) {
@@ -518,7 +553,7 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
 
     for (int iter = 0; iter < iters; ++iter) {
       if (VERBOSE) {
-        System.out.println("\nTEST: iter=" + (iter+1) + " of " + iters + " s=" + s);
+        System.out.println("\nTEST: iter=" + (iter + 1) + " of " + iters + " s=" + s);
       }
 
       Object[] queryPoints = nextPoints();
@@ -552,7 +587,8 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
         } else if (shapes[id] == null) {
           expected = false;
         } else {
-          expected = VALIDATOR.setRelation(queryRelation).testComponentQuery(queryPoly2D, shapes[id]);
+          expected =
+                  VALIDATOR.setRelation(queryRelation).testComponentQuery(queryPoly2D, shapes[id]);
         }
 
         if (hits.get(docID) != expected) {
@@ -587,7 +623,8 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
   }
 
   /** test random generated circles */
-  protected void verifyRandomDistanceQueries(IndexReader reader, Object... shapes) throws Exception {
+  protected void verifyRandomDistanceQueries(IndexReader reader, Object... shapes)
+          throws Exception {
     IndexSearcher s = newSearcher(reader);
 
     final int iters = scaledIterationCount(shapes.length);
@@ -624,7 +661,8 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
         } else if (shapes[id] == null) {
           expected = false;
         } else {
-          expected = VALIDATOR.setRelation(queryRelation).testComponentQuery(queryCircle2D, shapes[id]);
+          expected =
+                  VALIDATOR.setRelation(queryRelation).testComponentQuery(queryCircle2D, shapes[id]);
         }
 
         if (hits.get(docID) != expected) {
@@ -660,36 +698,43 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
 
   private FixedBitSet searchIndex(IndexSearcher s, Query query, int maxDoc) throws IOException {
     final FixedBitSet hits = new FixedBitSet(maxDoc);
-    s.search(query, new SimpleCollector() {
-
-      private int docBase;
-
-      @Override
-      public ScoreMode scoreMode() {
-        return ScoreMode.COMPLETE_NO_SCORES;
-      }
-
-      @Override
-      protected void doSetNextReader(LeafReaderContext context)  {
-        docBase = context.docBase;
-      }
-
-      @Override
-      public void collect(int doc) {
-        hits.set(docBase+doc);
-      }
-    });
+    s.search(
+            query,
+            new SimpleCollector() {
+
+              private int docBase;
+
+              @Override
+              public ScoreMode scoreMode() {
+                return ScoreMode.COMPLETE_NO_SCORES;
+              }
+
+              @Override
+              protected void doSetNextReader(LeafReaderContext context) {
+                docBase = context.docBase;
+              }
+
+              @Override
+              public void collect(int doc) {
+                hits.set(docBase + doc);
+              }
+            });
     return hits;
   }
-  
+
   protected abstract Validator getValidator();
 
-  protected static abstract class Encoder {
+  protected abstract static class Encoder {
     abstract double decodeX(int encoded);
+
     abstract double decodeY(int encoded);
+
     abstract double quantizeX(double raw);
+
     abstract double quantizeXCeil(double raw);
+
     abstract double quantizeY(double raw);
+
     abstract double quantizeYCeil(double raw);
   }
 
@@ -706,7 +751,7 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
   }
 
   /** validator class used to test query results against "ground truth" */
-  protected static abstract class Validator {
+  protected abstract static class Validator {
     Encoder encoder;
 
     Validator(Encoder encoder) {
@@ -714,7 +759,7 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
     }
 
     protected QueryRelation queryRelation = QueryRelation.INTERSECTS;
-    
+
     public abstract boolean testComponentQuery(Component2D line2d, Object shape);
 
     public Validator setRelation(QueryRelation relation) {
@@ -729,14 +774,16 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
         boolean contains;
         ShapeField.decodeTriangle(field.binaryValue().bytes, decodedTriangle);
         switch (decodedTriangle.type) {
-          case POINT: {
+          case POINT:
+          {
             double y = encoder.decodeY(decodedTriangle.aY);
             double x = encoder.decodeX(decodedTriangle.aX);
             intersects = query.contains(x, y);
             contains = intersects;
             break;
           }
-          case LINE: {
+          case LINE:
+          {
             double aY = encoder.decodeY(decodedTriangle.aY);
             double aX = encoder.decodeX(decodedTriangle.aX);
             double bY = encoder.decodeY(decodedTriangle.bY);
@@ -745,7 +792,8 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
             contains = query.containsLine(aX, aY, bX, bY);
             break;
           }
-          case TRIANGLE: {
+          case TRIANGLE:
+          {
             double aY = encoder.decodeY(decodedTriangle.aY);
             double aX = encoder.decodeX(decodedTriangle.aX);
             double bY = encoder.decodeY(decodedTriangle.bY);
@@ -757,7 +805,8 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
             break;
           }
           default:
-            throw new IllegalArgumentException("Unsupported triangle type :[" + decodedTriangle.type + "]");
+            throw new IllegalArgumentException(
+                    "Unsupported triangle type :[" + decodedTriangle.type + "]");
         }
         assertTrue((contains == intersects) || (contains == false && intersects == true));
         if (queryRelation == QueryRelation.DISJOINT && intersects) {
@@ -778,13 +827,15 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
         ShapeField.decodeTriangle(field.binaryValue().bytes, decodedTriangle);
         Component2D.WithinRelation relation;
         switch (decodedTriangle.type) {
-          case POINT: {
+          case POINT:
+          {
             double y = encoder.decodeY(decodedTriangle.aY);
             double x = encoder.decodeX(decodedTriangle.aX);
             relation = query.withinPoint(x, y);
             break;
           }
-          case LINE: {
+          case LINE:
+          {
             double aY = encoder.decodeY(decodedTriangle.aY);
             double aX = encoder.decodeX(decodedTriangle.aX);
             double bY = encoder.decodeY(decodedTriangle.bY);
@@ -792,18 +843,30 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
             relation = query.withinLine(aX, aY, decodedTriangle.ab, bX, bY);
             break;
           }
-          case TRIANGLE: {
+          case TRIANGLE:
+          {
             double aY = encoder.decodeY(decodedTriangle.aY);
             double aX = encoder.decodeX(decodedTriangle.aX);
             double bY = encoder.decodeY(decodedTriangle.bY);
             double bX = encoder.decodeX(decodedTriangle.bX);
             double cY = encoder.decodeY(decodedTriangle.cY);
             double cX = encoder.decodeX(decodedTriangle.cX);
-            relation = query.withinTriangle(aX, aY, decodedTriangle.ab, bX, bY, decodedTriangle.bc, cX, cY, decodedTriangle.ca);
+            relation =
+                    query.withinTriangle(
+                            aX,
+                            aY,
+                            decodedTriangle.ab,
+                            bX,
+                            bY,
+                            decodedTriangle.bc,
+                            cX,
+                            cY,
+                            decodedTriangle.ca);
             break;
           }
           default:
-            throw new IllegalArgumentException("Unsupported triangle type :[" + decodedTriangle.type + "]");
+            throw new IllegalArgumentException(
+                    "Unsupported triangle type :[" + decodedTriangle.type + "]");
         }
         if (relation == Component2D.WithinRelation.NOTWITHIN) {
           return relation;
diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java
index 3e44813..a591483 100644
--- a/lucene/core/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java
+++ b/lucene/core/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java
@@ -16,10 +16,12 @@
  */
 package org.apache.lucene.document;
 
-import java.util.Arrays;
-import java.util.Random;
+import static org.apache.lucene.geo.XYEncodingUtils.decode;
+import static org.apache.lucene.geo.XYEncodingUtils.encode;
 
 import com.carrotsearch.randomizedtesting.generators.RandomPicks;
+import java.util.Arrays;
+import java.util.Random;
 import org.apache.lucene.document.ShapeField.QueryRelation;
 import org.apache.lucene.geo.Component2D;
 import org.apache.lucene.geo.ShapeTestUtil;
@@ -33,11 +35,8 @@ import org.apache.lucene.geo.XYRectangle;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.util.TestUtil;
 
-import static org.apache.lucene.geo.XYEncodingUtils.decode;
-import static org.apache.lucene.geo.XYEncodingUtils.encode;
-
-/** Base test case for testing indexing and search functionality of cartesian geometry **/
-public abstract class BaseXYShapeTestCase extends BaseShapeTestCase {
+/** Base test case for testing indexing and search functionality of cartesian geometry * */
+public abstract class BaseXYShapeTestCase extends BaseSpatialTestCase {
   protected abstract ShapeType getShapeType();
 
   protected Object nextShape() {
@@ -46,8 +45,15 @@ public abstract class BaseXYShapeTestCase extends BaseShapeTestCase {
 
   /** factory method to create a new bounding box query */
   @Override
-  protected Query newRectQuery(String field, QueryRelation queryRelation, double minX, double maxX, double minY, double maxY) {
-    return XYShape.newBoxQuery(field, queryRelation, (float)minX, (float)maxX, (float)minY, (float)maxY);
+  protected Query newRectQuery(
+          String field,
+          QueryRelation queryRelation,
+          double minX,
+          double maxX,
+          double minY,
+          double maxY) {
+    return XYShape.newBoxQuery(
+            field, queryRelation, (float) minX, (float) maxX, (float) minY, (float) maxY);
   }
 
   /** factory method to create a new line query */
@@ -59,12 +65,14 @@ public abstract class BaseXYShapeTestCase extends BaseShapeTestCase {
   /** factory method to create a new polygon query */
   @Override
   protected Query newPolygonQuery(String field, QueryRelation queryRelation, Object... polygons) {
-    return XYShape.newPolygonQuery(field, queryRelation, Arrays.stream(polygons).toArray(XYPolygon[]::new));
+    return XYShape.newPolygonQuery(
+            field, queryRelation, Arrays.stream(polygons).toArray(XYPolygon[]::new));
   }
 
   @Override
   protected Query newPointsQuery(String field, QueryRelation queryRelation, Object... points) {
-    return XYShape.newPointQuery(field, queryRelation, Arrays.stream(points).toArray(float[][]::new));
+    return XYShape.newPointQuery(
+            field, queryRelation, Arrays.stream(points).toArray(float[][]::new));
   }
 
   @Override
@@ -76,7 +84,7 @@ public abstract class BaseXYShapeTestCase extends BaseShapeTestCase {
   protected Component2D toPoint2D(Object... points) {
     float[][] p = Arrays.stream(points).toArray(float[][]::new);
     XYPoint[] pointArray = new XYPoint[points.length];
-    for (int i =0; i < points.length; i++) {
+    for (int i = 0; i < points.length; i++) {
       pointArray[i] = new XYPoint(p[i][0], p[i][1]);
     }
     return XYGeometry.create(pointArray);
@@ -94,7 +102,8 @@ public abstract class BaseXYShapeTestCase extends BaseShapeTestCase {
 
   @Override
   protected Component2D toRectangle2D(double minX, double maxX, double minY, double maxY) {
-    return XYGeometry.create(new XYRectangle((float)minX, (float)maxX, (float)minY, (float)maxY));
+    return XYGeometry.create(
+            new XYRectangle((float) minX, (float) maxX, (float) minY, (float) maxY));
   }
 
   @Override
@@ -109,22 +118,22 @@ public abstract class BaseXYShapeTestCase extends BaseShapeTestCase {
 
   @Override
   protected double rectMinX(Object rect) {
-    return ((XYRectangle)rect).minX;
+    return ((XYRectangle) rect).minX;
   }
 
   @Override
   protected double rectMaxX(Object rect) {
-    return ((XYRectangle)rect).maxX;
+    return ((XYRectangle) rect).maxX;
   }
 
   @Override
   protected double rectMinY(Object rect) {
-    return ((XYRectangle)rect).minY;
+    return ((XYRectangle) rect).minY;
   }
 
   @Override
   protected double rectMaxY(Object rect) {
-    return ((XYRectangle)rect).maxY;
+    return ((XYRectangle) rect).maxY;
   }
 
   @Override
@@ -149,8 +158,8 @@ public abstract class BaseXYShapeTestCase extends BaseShapeTestCase {
     int numPoints = TestUtil.nextInt(random, 1, 20);
     float[][] points = new float[numPoints][2];
     for (int i = 0; i < numPoints; i++) {
-      points[i][0] =  ShapeTestUtil.nextFloat(random);
-      points[i][1] =  ShapeTestUtil.nextFloat(random);
+      points[i][0] = ShapeTestUtil.nextFloat(random);
+      points[i][1] = ShapeTestUtil.nextFloat(random);
     }
     return points;
   }
@@ -172,6 +181,7 @@ public abstract class BaseXYShapeTestCase extends BaseShapeTestCase {
       double decodeY(int encoded) {
         return decode(encoded);
       }
+
       @Override
       double quantizeX(double raw) {
         return decode(encode((float) raw));
@@ -226,6 +236,7 @@ public abstract class BaseXYShapeTestCase extends BaseShapeTestCase {
     };
 
     static ShapeType[] subList;
+
     static {
       subList = new ShapeType[] {POINT, LINE, POLYGON};
     }
diff --git a/lucene/core/src/test/org/apache/lucene/document/TestLatLonDocValuesMultiPointPointQueries.java b/lucene/core/src/test/org/apache/lucene/document/TestLatLonDocValuesMultiPointPointQueries.java
new file mode 100644
index 0000000..e66a420
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/document/TestLatLonDocValuesMultiPointPointQueries.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.document;
+
+import org.apache.lucene.document.ShapeField.QueryRelation;
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.Point;
+
+/**
+ * random bounding box, line, and polygon query tests for random indexed arrays of {@code latitude,
+ * longitude} points
+ */
+public class TestLatLonDocValuesMultiPointPointQueries extends BaseLatLonDocValueTestCase {
+
+  @Override
+  protected ShapeType getShapeType() {
+    return ShapeType.POINT;
+  }
+
+  @Override
+  protected Object nextShape() {
+    int n = random().nextInt(4) + 1;
+    Point[] points = new Point[n];
+    for (int i = 0; i < n; i++) {
+      points[i] = (Point) ShapeType.POINT.nextShape();
+    }
+    return points;
+  }
+
+  @Override
+  protected Field[] createIndexableFields(String name, Object o) {
+    Point[] points = (Point[]) o;
+    Field[] fields = new Field[points.length];
+    for (int i = 0; i < points.length; i++) {
+      fields[i] = new LatLonDocValuesField(FIELD_NAME, points[i].getLat(), points[i].getLon());
+    }
+    return fields;
+  }
+
+  @Override
+  public Validator getValidator() {
+    return new MultiPointValidator(ENCODER);
+  }
+
+  protected class MultiPointValidator extends Validator {
+    TestLatLonPointShapeQueries.PointValidator POINTVALIDATOR;
+
+    MultiPointValidator(Encoder encoder) {
+      super(encoder);
+      POINTVALIDATOR = new TestLatLonPointShapeQueries.PointValidator(encoder);
+    }
+
+    @Override
+    public Validator setRelation(QueryRelation relation) {
+      super.setRelation(relation);
+      POINTVALIDATOR.queryRelation = relation;
+      return this;
+    }
+
+    @Override
+    public boolean testComponentQuery(Component2D query, Object shape) {
+      Point[] points = (Point[]) shape;
+      for (Point p : points) {
+        boolean b = POINTVALIDATOR.testComponentQuery(query, p);
+        if (b == true && queryRelation == QueryRelation.INTERSECTS) {
+          return true;
+        } else if (b == true && queryRelation == QueryRelation.CONTAINS) {
+          return true;
+        } else if (b == false && queryRelation == QueryRelation.DISJOINT) {
+          return false;
+        } else if (b == false && queryRelation == QueryRelation.WITHIN) {
+          return false;
+        }
+      }
+      return queryRelation != QueryRelation.INTERSECTS && queryRelation != QueryRelation.CONTAINS;
+    }
+  }
+
+  @Slow
+  @Nightly
+  @Override
+  public void testRandomBig() throws Exception {
+    doTestRandom(10000);
+  }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/document/TestLatLonDocValuesPointPointQueries.java b/lucene/core/src/test/org/apache/lucene/document/TestLatLonDocValuesPointPointQueries.java
new file mode 100644
index 0000000..1e8532b
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/document/TestLatLonDocValuesPointPointQueries.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.document;
+
+import org.apache.lucene.document.ShapeField.QueryRelation;
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.Point;
+
+/**
+ * random bounding box, line, and polygon query tests for random indexed arrays of {@code latitude,
+ * longitude} points
+ */
+public class TestLatLonDocValuesPointPointQueries extends BaseLatLonDocValueTestCase {
+
+  @Override
+  protected ShapeType getShapeType() {
+    return ShapeType.POINT;
+  }
+
+  @Override
+  protected Field[] createIndexableFields(String name, Object o) {
+    Point point = (Point) o;
+    Field[] fields = new Field[1];
+    fields[0] = new LatLonDocValuesField(FIELD_NAME, point.getLat(), point.getLon());
+    return fields;
+  }
+
+  @Override
+  protected Validator getValidator() {
+    return new TestLatLonPointShapeQueries.PointValidator(this.ENCODER);
+  }
+
+  protected static class PointValidator extends Validator {
+    protected PointValidator(Encoder encoder) {
+      super(encoder);
+    }
+
+    @Override
+    public boolean testComponentQuery(Component2D query, Object shape) {
+      Point p = (Point) shape;
+      if (queryRelation == QueryRelation.CONTAINS) {
+        return testWithinQuery(
+                query, LatLonShape.createIndexableFields("dummy", p.getLat(), p.getLon()))
+            == Component2D.WithinRelation.CANDIDATE;
+      }
+      return testComponentQuery(
+          query, LatLonShape.createIndexableFields("dummy", p.getLat(), p.getLon()));
+    }
+  }
+
+  @Slow
+  @Nightly
+  @Override
+  public void testRandomBig() throws Exception {
+    doTestRandom(10000);
+  }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/document/TestLatLonMultiPointPointQueries.java b/lucene/core/src/test/org/apache/lucene/document/TestLatLonMultiPointPointQueries.java
new file mode 100644
index 0000000..808fb9d
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/document/TestLatLonMultiPointPointQueries.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.document;
+
+import org.apache.lucene.document.ShapeField.QueryRelation;
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.Point;
+
+/**
+ * random bounding box, line, and polygon query tests for random indexed arrays of {@code latitude,
+ * longitude} points
+ */
+public class TestLatLonMultiPointPointQueries extends BaseLatLonPointTestCase {
+
+  @Override
+  protected ShapeType getShapeType() {
+    return ShapeType.POINT;
+  }
+
+  @Override
+  protected Object nextShape() {
+    int n = random().nextInt(4) + 1;
+    Point[] points = new Point[n];
+    for (int i = 0; i < n; i++) {
+      points[i] = (Point) ShapeType.POINT.nextShape();
+    }
+    return points;
+  }
+
+  @Override
+  protected Field[] createIndexableFields(String name, Object o) {
+    Point[] points = (Point[]) o;
+    Field[] fields = new Field[points.length];
+    for (int i = 0; i < points.length; i++) {
+      fields[i] = new LatLonPoint(FIELD_NAME, points[i].getLat(), points[i].getLon());
+    }
+    return fields;
+  }
+
+  @Override
+  public Validator getValidator() {
+    return new MultiPointValidator(ENCODER);
+  }
+
+  protected class MultiPointValidator extends Validator {
+    TestLatLonPointShapeQueries.PointValidator POINTVALIDATOR;
+
+    MultiPointValidator(Encoder encoder) {
+      super(encoder);
+      POINTVALIDATOR = new TestLatLonPointShapeQueries.PointValidator(encoder);
+    }
+
+    @Override
+    public Validator setRelation(QueryRelation relation) {
+      super.setRelation(relation);
+      POINTVALIDATOR.queryRelation = relation;
+      return this;
+    }
+
+    @Override
+    public boolean testComponentQuery(Component2D query, Object shape) {
+      Point[] points = (Point[]) shape;
+      for (Point p : points) {
+        boolean b = POINTVALIDATOR.testComponentQuery(query, p);
+        if (b == true && queryRelation == QueryRelation.INTERSECTS) {
+          return true;
+        } else if (b == true && queryRelation == QueryRelation.CONTAINS) {
+          return true;
+        } else if (b == false && queryRelation == QueryRelation.DISJOINT) {
+          return false;
+        } else if (b == false && queryRelation == QueryRelation.WITHIN) {
+          return false;
+        }
+      }
+      return queryRelation != QueryRelation.INTERSECTS && queryRelation != QueryRelation.CONTAINS;
+    }
+  }
+
+  @Slow
+  @Nightly
+  @Override
+  public void testRandomBig() throws Exception {
+    doTestRandom(10000);
+  }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/document/TestLatLonPointPointQueries.java b/lucene/core/src/test/org/apache/lucene/document/TestLatLonPointPointQueries.java
new file mode 100644
index 0000000..752f9a0
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/document/TestLatLonPointPointQueries.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.document;
+
+import org.apache.lucene.document.ShapeField.QueryRelation;
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.Point;
+
+/**
+ * random bounding box, line, and polygon query tests for random indexed arrays of {@code latitude,
+ * longitude} points
+ */
+public class TestLatLonPointPointQueries extends BaseLatLonPointTestCase {
+
+  @Override
+  protected ShapeType getShapeType() {
+    return ShapeType.POINT;
+  }
+
+  @Override
+  protected Validator getValidator() {
+    return new TestLatLonPointShapeQueries.PointValidator(this.ENCODER);
+  }
+
+  @Override
+  protected Field[] createIndexableFields(String name, Object o) {
+    Point point = (Point) o;
+    return new Field[] {new LatLonPoint(FIELD_NAME, point.getLat(), point.getLon())};
+  }
+
+  protected static class PointValidator extends Validator {
+    protected PointValidator(Encoder encoder) {
+      super(encoder);
+    }
+
+    @Override
+    public boolean testComponentQuery(Component2D query, Object shape) {
+      Point p = (Point) shape;
+      if (queryRelation == QueryRelation.CONTAINS) {
+        return testWithinQuery(
+                query, LatLonShape.createIndexableFields("dummy", p.getLat(), p.getLon()))
+            == Component2D.WithinRelation.CANDIDATE;
+      }
+      return testComponentQuery(
+          query, LatLonShape.createIndexableFields("dummy", p.getLat(), p.getLon()));
+    }
+  }
+
+  @Slow
+  @Nightly
+  @Override
+  public void testRandomBig() throws Exception {
+    doTestRandom(10000);
+  }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java b/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java
index fc08130..0d00a44 100644
--- a/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java
+++ b/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java
@@ -425,6 +425,15 @@ public class TestLatLonShape extends LuceneTestCase {
     RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
     Document document = new Document();
     Point p = GeoTestUtil.nextPoint();
+    double qLat =
+        p.getLat() == GeoUtils.MAX_LAT_INCL
+            ? p.getLat()
+            : GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitudeCeil(p.getLat()));
+    double qLon =
+        p.getLon() == GeoUtils.MAX_LON_INCL
+            ? p.getLon()
+            : GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitudeCeil(p.getLon()));
+    p = new Point(qLat, qLon);
     Field[] fields = LatLonShape.createIndexableFields(FIELDNAME, p.getLat(), p.getLon());
     for (Field f : fields) {
       document.add(f);
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestLatLonDocValuesQueries.java b/lucene/core/src/test/org/apache/lucene/search/TestLatLonDocValuesQueries.java
index ebb5deb..53b1c43 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestLatLonDocValuesQueries.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestLatLonDocValuesQueries.java
@@ -18,6 +18,7 @@ package org.apache.lucene.search;
 
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.LatLonDocValuesField;
+import org.apache.lucene.document.ShapeField;
 import org.apache.lucene.geo.BaseGeoPointTestCase;
 import org.apache.lucene.geo.GeoEncodingUtils;
 import org.apache.lucene.geo.LatLonGeometry;
@@ -47,7 +48,8 @@ public class TestLatLonDocValuesQueries extends BaseGeoPointTestCase {
 
   @Override
   protected Query newGeometryQuery(String field, LatLonGeometry... geometry) {
-    return LatLonDocValuesField.newSlowGeometryQuery(field, geometry);
+    return LatLonDocValuesField.newSlowGeometryQuery(
+        field, ShapeField.QueryRelation.INTERSECTS, geometry);
   }
 
   @Override
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestLatLonPointQueries.java b/lucene/core/src/test/org/apache/lucene/search/TestLatLonPointQueries.java
index 265367f..643a24f 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestLatLonPointQueries.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestLatLonPointQueries.java
@@ -20,6 +20,7 @@ import java.io.IOException;
 
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.LatLonPoint;
+import org.apache.lucene.document.ShapeField;
 import org.apache.lucene.geo.BaseGeoPointTestCase;
 import org.apache.lucene.geo.GeoEncodingUtils;
 import org.apache.lucene.geo.LatLonGeometry;
@@ -54,7 +55,7 @@ public class TestLatLonPointQueries extends BaseGeoPointTestCase {
 
   @Override
   protected Query newGeometryQuery(String field, LatLonGeometry... geometry) {
-    return LatLonPoint.newGeometryQuery(field, geometry);
+    return LatLonPoint.newGeometryQuery(field, ShapeField.QueryRelation.INTERSECTS, geometry);
   }
 
   @Override