You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ho...@apache.org on 2016/03/01 18:07:29 UTC

[46/50] [abbrv] lucene-solr git commit: LUCENE-7057: cleanup some sandiness around LatLonPoint

LUCENE-7057: cleanup some sandiness around LatLonPoint


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/2264600f
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/2264600f
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/2264600f

Branch: refs/heads/jira/SOLR-445
Commit: 2264600ffe4649abb0edbe7a6882ffc82f6e918b
Parents: b6ec959
Author: Robert Muir <rm...@apache.org>
Authored: Tue Mar 1 07:04:16 2016 -0500
Committer: Robert Muir <rm...@apache.org>
Committed: Tue Mar 1 07:04:45 2016 -0500

----------------------------------------------------------------------
 .../org/apache/lucene/document/LatLonPoint.java | 212 +++++++++++------
 .../document/LatLonPointDistanceQuery.java      | 193 ++++++++++++++++
 .../document/LatLonPointInPolygonQuery.java     | 226 +++++++++++++++++++
 .../org/apache/lucene/document/package.html     |   7 +-
 .../lucene/search/PointDistanceQuery.java       | 179 ---------------
 .../lucene/search/PointInPolygonQuery.java      | 207 -----------------
 .../apache/lucene/document/TestLatLonPoint.java | 197 ++++++++--------
 .../document/TestLatLonPointDistanceQuery.java  | 190 ++++++++++++++++
 .../lucene/search/TestLatLonPointQueries.java   |  62 ++---
 9 files changed, 874 insertions(+), 599 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2264600f/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPoint.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPoint.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPoint.java
index 92b4a3a..b0902f5 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPoint.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPoint.java
@@ -19,11 +19,10 @@ package org.apache.lucene.document;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.NumericUtils;
 
+import org.apache.lucene.index.FieldInfo;
 import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.ConstantScoreQuery;
-import org.apache.lucene.search.PointDistanceQuery;
-import org.apache.lucene.search.PointInPolygonQuery;
 import org.apache.lucene.search.PointRangeQuery;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.spatial.util.GeoUtils;
@@ -49,36 +48,45 @@ import org.apache.lucene.spatial.util.GeoUtils;
 // 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 {
+  /**
+   * Type for an indexed LatLonPoint
+   * <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
+   */
+  public void setLocationValue(double latitude, double longitude) {
+    byte[] bytes = new byte[8];
+    NumericUtils.intToBytes(encodeLatitude(latitude), bytes, 0);
+    NumericUtils.intToBytes(encodeLongitude(longitude), bytes, Integer.BYTES);
+    fieldsData = new BytesRef(bytes);
+  }
 
   /** 
-   * Creates a new LatLonPoint with the specified lat and lon
+   * Creates a new LatLonPoint with the specified latitude and longitude
    * @param name field name
-   * @param lat double latitude
-   * @param lon double longitude
-   * @throws IllegalArgumentException if the field name is null or lat or lon are out of bounds
+   * @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
    */
-  public LatLonPoint(String name, double lat, double lon) {
+  public LatLonPoint(String name, double latitude, double longitude) {
     super(name, TYPE);
-    if (GeoUtils.isValidLat(lat) == false) {
-      throw new IllegalArgumentException("invalid lat (" + lat + "): must be -90 to 90");
-    }
-    if (GeoUtils.isValidLon(lon) == false) {
-      throw new IllegalArgumentException("invalid lon (" + lon + "): must be -180 to 180");
-    }
-    byte[] bytes = new byte[8];
-    NumericUtils.intToBytes(encodeLat(lat), bytes, 0);
-    NumericUtils.intToBytes(encodeLon(lon), bytes, Integer.BYTES);
-    fieldsData = new BytesRef(bytes);
+    setLocationValue(latitude, longitude);
   }
 
   private static final int BITS = 32;
-  private static final double LON_SCALE = (0x1L<<BITS)/360.0D;
-  private static final double LAT_SCALE = (0x1L<<BITS)/180.0D;
+  private static final double LONGITUDE_SCALE = (0x1L<<BITS)/360.0D;
+  private static final double LATITUDE_SCALE = (0x1L<<BITS)/180.0D;
   
   @Override
   public String toString() {
@@ -89,9 +97,9 @@ public class LatLonPoint extends Field {
     result.append(':');
 
     BytesRef bytes = (BytesRef) fieldsData;
-    result.append(decodeLat(BytesRef.deepCopyOf(bytes).bytes, 0));
+    result.append(decodeLatitude(BytesRef.deepCopyOf(bytes).bytes, 0));
     result.append(',');
-    result.append(decodeLon(BytesRef.deepCopyOf(bytes).bytes, Integer.BYTES));
+    result.append(decodeLongitude(BytesRef.deepCopyOf(bytes).bytes, Integer.BYTES));
 
     result.append('>');
     return result.toString();
@@ -99,79 +107,125 @@ public class LatLonPoint extends Field {
 
   // public helper methods (e.g. for queries)
 
-  /** Quantizes double (64 bit) latitude into 32 bits */
-  public static int encodeLat(double lat) {
-    assert GeoUtils.isValidLat(lat): "lat=" + lat;
-    long x = (long) (lat * LAT_SCALE);
-    assert x < Integer.MAX_VALUE: "lat=" + lat + " mapped to Integer.MAX_VALUE + " + (x - Integer.MAX_VALUE);
-    assert x > Integer.MIN_VALUE: "lat=" + lat + " mapped to Integer.MIN_VALUE";
-    return (int) x;
+  /** 
+   * Quantizes double (64 bit) latitude into 32 bits 
+   * @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
+   */
+  public static int encodeLatitude(double latitude) {
+    if (GeoUtils.isValidLat(latitude) == false) {
+      throw new IllegalArgumentException("invalid latitude: " + latitude + ", must be -90 to 90");
+    }
+    // the maximum possible value cannot be encoded without overflow
+    if (latitude == 90.0D) {
+      latitude = Math.nextDown(latitude);
+    }
+    return Math.toIntExact((long) (latitude * LATITUDE_SCALE));
   }
 
-  /** Quantizes double (64 bit) longitude into 32 bits */
-  public static int encodeLon(double lon) {
-    assert GeoUtils.isValidLon(lon): "lon=" + lon;
-    long x = (long) (lon * LON_SCALE);
-    assert x < Integer.MAX_VALUE;
-    assert x > Integer.MIN_VALUE;
-    return (int) x;
+  /** 
+   * Quantizes double (64 bit) longitude into 32 bits 
+   * @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
+   */
+  public static int encodeLongitude(double longitude) {
+    if (GeoUtils.isValidLon(longitude) == false) {
+      throw new IllegalArgumentException("invalid longitude: " + longitude + ", must be -180 to 180");
+    }
+    // the maximum possible value cannot be encoded without overflow
+    if (longitude == 180.0D) {
+      longitude = Math.nextDown(longitude);
+    }
+    return Math.toIntExact((long) (longitude * LONGITUDE_SCALE));
   }
 
-  /** Turns quantized value from {@link #encodeLat} back into a double. */
-  public static double decodeLat(int x) {
-    return x / LAT_SCALE;
+  /** 
+   * Turns quantized value from {@link #encodeLatitude} back into a double. 
+   * @param encoded encoded value: 32-bit quantized value.
+   * @return decoded latitude value.
+   */
+  public static double decodeLatitude(int encoded) {
+    double result = encoded / LATITUDE_SCALE;
+    assert GeoUtils.isValidLat(result);
+    return result;
   }
   
-  /** Turns quantized value from byte array back into a double. */
-  public static double decodeLat(byte[] src, int offset) {
-    return decodeLat(NumericUtils.bytesToInt(src, offset));
+  /** 
+   * 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.
+   */
+  public static double decodeLatitude(byte[] src, int offset) {
+    return decodeLatitude(NumericUtils.bytesToInt(src, offset));
   }
 
-  /** Turns quantized value from {@link #encodeLon} back into a double. */
-  public static double decodeLon(int x) {
-    return x / LON_SCALE;
+  /** 
+   * Turns quantized value from {@link #encodeLongitude} back into a double. 
+   * @param encoded encoded value: 32-bit quantized value.
+   * @return decoded longitude value.
+   */  
+  public static double decodeLongitude(int encoded) {
+    double result = encoded / LONGITUDE_SCALE;
+    assert GeoUtils.isValidLon(result);
+    return result;
   }
 
-  /** Turns quantized value from byte array back into a double. */
-  public static double decodeLon(byte[] src, int offset) {
-    return decodeLon(NumericUtils.bytesToInt(src, offset));
+  /** 
+   * 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.
+   */
+  public static double decodeLongitude(byte[] src, int offset) {
+    return decodeLongitude(NumericUtils.bytesToInt(src, offset));
   }
   
   /** sugar encodes a single point as a 2D byte array */
-  private static byte[][] encode(double lat, double lon) {
+  private static byte[][] encode(double latitude, double longitude) {
     byte[][] bytes = new byte[2][];
     bytes[0] = new byte[4];
-    NumericUtils.intToBytes(encodeLat(lat), bytes[0], 0);
+    NumericUtils.intToBytes(encodeLatitude(latitude), bytes[0], 0);
     bytes[1] = new byte[4];
-    NumericUtils.intToBytes(encodeLon(lon), bytes[1], 0);
+    NumericUtils.intToBytes(encodeLongitude(longitude), bytes[1], 0);
     return bytes;
   }
-   
+
+  /** helper: checks a fieldinfo and throws exception if its definitely not a LatLonPoint */
+  static void checkCompatible(FieldInfo fieldInfo) {
+    if (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() != 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?");
+    }
+  }
+
   // static methods for generating queries
 
   /**
    * Create a query for matching a bounding box.
    * <p>
    * The box may cross over the dateline.
+   * @param field field name. cannot 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.
+   * @param minLongitude longitude lower bound: must be within standard +/-180 coordinate bounds.
+   * @param maxLongitude longitude upper bound: must be within standard +/-180 coordinate bounds.
+   * @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 minLat, double maxLat, double minLon, double maxLon) {
-    if (GeoUtils.isValidLat(minLat) == false) {
-      throw new IllegalArgumentException("minLat=" + minLat + " is not a valid latitude");
-    }
-    if (GeoUtils.isValidLat(maxLat) == false) {
-      throw new IllegalArgumentException("maxLat=" + maxLat + " is not a valid latitude");
-    }
-    if (GeoUtils.isValidLon(minLon) == false) {
-      throw new IllegalArgumentException("minLon=" + minLon + " is not a valid longitude");
-    }
-    if (GeoUtils.isValidLon(maxLon) == false) {
-      throw new IllegalArgumentException("maxLon=" + maxLon + " is not a valid longitude");
-    }
-    
-    byte[][] lower = encode(minLat, minLon);
-    byte[][] upper = encode(maxLat, maxLon);
+  public static Query newBoxQuery(String field, double minLatitude, double maxLatitude, double minLongitude, double maxLongitude) {
+    byte[][] lower = encode(minLatitude, minLongitude);
+    byte[][] upper = encode(maxLatitude, maxLongitude);
     // Crosses date line: we just rewrite into OR of two bboxes, with longitude as an open range:
-    if (maxLon < minLon) {
+    if (maxLongitude < minLongitude) {
       // Disable coord here because a multi-valued doc could match both rects and get unfairly boosted:
       BooleanQuery.Builder q = new BooleanQuery.Builder();
       q.setDisableCoord(true);
@@ -198,9 +252,9 @@ public class LatLonPoint extends Field {
       @Override
       protected String toString(int dimension, byte[] value) {
         if (dimension == 0) {
-          return Double.toString(decodeLat(value, 0));
+          return Double.toString(decodeLatitude(value, 0));
         } else if (dimension == 1) {
-          return Double.toString(decodeLon(value, 0));
+          return Double.toString(decodeLongitude(value, 0));
         } else {
           throw new AssertionError();
         }
@@ -210,17 +264,31 @@ public class LatLonPoint extends Field {
   
   /**
    * Create a query for matching points within the specified distance of the supplied location.
+   * @param field field name. cannot 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.
+   * @return query matching points within this distance
+   * @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) {
-    return new PointDistanceQuery(field, latitude, longitude, radiusMeters);
+    return new LatLonPointDistanceQuery(field, latitude, longitude, radiusMeters);
   }
   
   /** 
    * Create a query for matching a polygon.
    * <p>
    * The supplied {@code polyLats}/{@code polyLons} must be clockwise or counter-clockwise.
+   * @param field field name. cannot be null.
+   * @param polyLats latitude values for points of the polygon: must be within standard +/-90 coordinate bounds.
+   * @param polyLons longitude values for points of the polygon: must be within standard +/-180 coordinate bounds.
+   * @return query matching points within this polygon
+   * @throws IllegalArgumentException if {@code field} is null, {@code polyLats} is null or has invalid coordinates, 
+   *                                  {@code polyLons} is null or has invalid coordinates, if {@code polyLats} has a different
+   *                                  length than {@code polyLons}, if the polygon has less than 4 points, or if polygon is 
+   *                                  not closed (first and last points should be the same)
    */
   public static Query newPolygonQuery(String field, double[] polyLats, double[] polyLons) {
-    return new PointInPolygonQuery(field, polyLats, polyLons);
+    return new LatLonPointInPolygonQuery(field, polyLats, polyLons);
   }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2264600f/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointDistanceQuery.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointDistanceQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointDistanceQuery.java
new file mode 100644
index 0000000..de4805b
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointDistanceQuery.java
@@ -0,0 +1,193 @@
+/*
+ * 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 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.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.Weight;
+import org.apache.lucene.spatial.util.GeoDistanceUtils;
+import org.apache.lucene.spatial.util.GeoRect;
+import org.apache.lucene.spatial.util.GeoUtils;
+import org.apache.lucene.util.DocIdSetBuilder;
+
+/**
+ * Distance query for {@link LatLonPoint}.
+ */
+final class LatLonPointDistanceQuery extends Query {
+  final String field;
+  final double latitude;
+  final double longitude;
+  final double radiusMeters;
+
+  public LatLonPointDistanceQuery(String field, double latitude, double longitude, double radiusMeters) {
+    if (field == null) {
+      throw new IllegalArgumentException("field cannot be null");
+    }
+    if (Double.isFinite(radiusMeters) == false || radiusMeters < 0) {
+      throw new IllegalArgumentException("radiusMeters: '" + radiusMeters + "' is invalid");
+    }
+    if (GeoUtils.isValidLat(latitude) == false) {
+      throw new IllegalArgumentException("latitude: '" + latitude + "' is invalid");
+    }
+    if (GeoUtils.isValidLon(longitude) == false) {
+      throw new IllegalArgumentException("longitude: '" + longitude + "' is invalid");
+    }
+    this.field = field;
+    this.latitude = latitude;
+    this.longitude = longitude;
+    this.radiusMeters = radiusMeters;
+  }
+
+  @Override
+  public Weight createWeight(IndexSearcher searcher, boolean needsScores) throws IOException {
+    GeoRect box = GeoUtils.circleToBBox(longitude, latitude, radiusMeters);
+    final GeoRect box1;
+    final GeoRect box2;
+
+    // crosses dateline: split
+    if (box.maxLon < box.minLon) {
+      box1 = new GeoRect(-180.0, box.maxLon, box.minLat, box.maxLat);
+      box2 = new GeoRect(box.minLon, 180.0, box.minLat, box.maxLat);
+    } else {
+      box1 = box;
+      box2 = null;
+    }
+
+    return new ConstantScoreWeight(this) {
+
+      @Override
+      public Scorer scorer(LeafReaderContext context) throws IOException {
+        LeafReader reader = context.reader();
+        PointValues values = reader.getPointValues();
+        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);
+        
+        DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc());
+        values.intersect(field,
+                         new IntersectVisitor() {
+                           @Override
+                           public void visit(int docID) {
+                             result.add(docID);
+                           }
+
+                           @Override
+                           public void visit(int docID, byte[] packedValue) {
+                             assert packedValue.length == 8;
+                             double lat = LatLonPoint.decodeLatitude(packedValue, 0);
+                             double lon = LatLonPoint.decodeLongitude(packedValue, Integer.BYTES);
+                             if (GeoDistanceUtils.haversin(latitude, longitude, lat, lon) <= radiusMeters) {
+                               visit(docID);
+                             }
+                           }
+                           
+                           // algorithm: we create a bounding box (two bounding boxes if we cross the dateline).
+                           // 1. check our bounding box(es) first. if the subtree is entirely outside of those, bail.
+                           // 2. see if the subtree is fully contained. if the subtree is enormous along the x axis, wrapping half way around the world, etc: then this can't work, just go to step 3.
+                           // 3. recurse naively.
+                           @Override
+                           public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
+                             double latMin = LatLonPoint.decodeLatitude(minPackedValue, 0);
+                             double lonMin = LatLonPoint.decodeLongitude(minPackedValue, Integer.BYTES);
+                             double latMax = LatLonPoint.decodeLatitude(maxPackedValue, 0);
+                             double lonMax = LatLonPoint.decodeLongitude(maxPackedValue, Integer.BYTES);
+                             
+                             if ((latMax < box1.minLat || lonMax < box1.minLon || latMin > box1.maxLat || lonMin > box1.maxLon) && 
+                                 (box2 == null || latMax < box2.minLat || lonMax < box2.minLon || latMin > box2.maxLat || lonMin > box2.maxLon)) {
+                               // we are fully outside of bounding box(es), don't proceed any further.
+                               return Relation.CELL_OUTSIDE_QUERY;
+                             } else if (lonMax - longitude < 90 && longitude - lonMin < 90 &&
+                                 GeoDistanceUtils.haversin(latitude, longitude, latMin, lonMin) <= radiusMeters &&
+                                 GeoDistanceUtils.haversin(latitude, longitude, latMin, lonMax) <= radiusMeters &&
+                                 GeoDistanceUtils.haversin(latitude, longitude, latMax, lonMin) <= radiusMeters &&
+                                 GeoDistanceUtils.haversin(latitude, longitude, latMax, lonMax) <= radiusMeters) {
+                               // we are fully enclosed, collect everything within this subtree
+                               return Relation.CELL_INSIDE_QUERY;
+                             } else {
+                               // recurse: its inside our bounding box(es), but not fully, or it wraps around.
+                               return Relation.CELL_CROSSES_QUERY;
+                             }
+                           }
+                         });
+
+        return new ConstantScoreScorer(this, score(), result.build().iterator());
+      }
+    };
+  }
+
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = super.hashCode();
+    result = prime * result + field.hashCode();
+    long temp;
+    temp = Double.doubleToLongBits(latitude);
+    result = prime * result + (int) (temp ^ (temp >>> 32));
+    temp = Double.doubleToLongBits(longitude);
+    result = prime * result + (int) (temp ^ (temp >>> 32));
+    temp = Double.doubleToLongBits(radiusMeters);
+    result = prime * result + (int) (temp ^ (temp >>> 32));
+    return result;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) return true;
+    if (!super.equals(obj)) return false;
+    if (getClass() != obj.getClass()) return false;
+    LatLonPointDistanceQuery other = (LatLonPointDistanceQuery) obj;
+    if (!field.equals(other.field)) return false;
+    if (Double.doubleToLongBits(latitude) != Double.doubleToLongBits(other.latitude)) return false;
+    if (Double.doubleToLongBits(longitude) != Double.doubleToLongBits(other.longitude)) return false;
+    if (Double.doubleToLongBits(radiusMeters) != Double.doubleToLongBits(other.radiusMeters)) return false;
+    return true;
+  }
+
+  @Override
+  public String toString(String field) {
+    StringBuilder sb = new StringBuilder();
+    if (!this.field.equals(field)) {
+      sb.append(this.field);
+      sb.append(':');
+    }
+    sb.append(latitude);
+    sb.append(",");
+    sb.append(longitude);
+    sb.append(" +/- ");
+    sb.append(radiusMeters);
+    sb.append(" meters");
+    return sb.toString();
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2264600f/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java
new file mode 100644
index 0000000..d75e615
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java
@@ -0,0 +1,226 @@
+/*
+ * 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.Arrays;
+
+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.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.Weight;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.util.DocIdSetBuilder;
+import org.apache.lucene.spatial.util.GeoRelationUtils;
+import org.apache.lucene.spatial.util.GeoUtils;
+
+/** Finds all previously indexed points that fall within the specified polygon.
+ *
+ *  <p>The field must be indexed with using {@link org.apache.lucene.document.LatLonPoint} added per document.
+ *
+ *  @lucene.experimental */
+
+final class LatLonPointInPolygonQuery extends Query {
+  final String field;
+  final double minLat;
+  final double maxLat;
+  final double minLon;
+  final double maxLon;
+  final double[] polyLats;
+  final double[] polyLons;
+
+  /** The lats/lons must be clockwise or counter-clockwise. */
+  public LatLonPointInPolygonQuery(String field, double[] polyLats, double[] polyLons) {
+    this.field = field;
+    if (field == null) {
+      throw new IllegalArgumentException("field cannot be null");
+    }
+    if (polyLats == null) {
+      throw new IllegalArgumentException("polyLats cannot be null");
+    }
+    if (polyLons == null) {
+      throw new IllegalArgumentException("polyLons cannot be null");
+    }
+    if (polyLats.length != polyLons.length) {
+      throw new IllegalArgumentException("polyLats and polyLons must be equal length");
+    }
+    if (polyLats.length < 4) {
+      throw new IllegalArgumentException("at least 4 polygon points required");
+    }
+    if (polyLats[0] != polyLats[polyLats.length-1]) {
+      throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): polyLats[0]=" + polyLats[0] + " polyLats[" + (polyLats.length-1) + "]=" + polyLats[polyLats.length-1]);
+    }
+    if (polyLons[0] != polyLons[polyLons.length-1]) {
+      throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): polyLons[0]=" + polyLons[0] + " polyLons[" + (polyLons.length-1) + "]=" + polyLons[polyLons.length-1]);
+    }
+
+    this.polyLats = polyLats;
+    this.polyLons = polyLons;
+
+    // TODO: we could also compute the maximal inner bounding box, to make relations faster to compute?
+
+    double minLon = Double.POSITIVE_INFINITY;
+    double minLat = Double.POSITIVE_INFINITY;
+    double maxLon = Double.NEGATIVE_INFINITY;
+    double maxLat = Double.NEGATIVE_INFINITY;
+    for(int i=0;i<polyLats.length;i++) {
+      double lat = polyLats[i];
+      if (GeoUtils.isValidLat(lat) == false) {
+        throw new IllegalArgumentException("polyLats[" + i + "]=" + lat + " is not a valid latitude");
+      }
+      minLat = Math.min(minLat, lat);
+      maxLat = Math.max(maxLat, lat);
+      double lon = polyLons[i];
+      if (GeoUtils.isValidLon(lon) == false) {
+        throw new IllegalArgumentException("polyLons[" + i + "]=" + lat + " is not a valid longitude");
+      }
+      minLon = Math.min(minLon, lon);
+      maxLon = Math.max(maxLon, lon);
+    }
+    this.minLon = minLon;
+    this.maxLon = maxLon;
+    this.minLat = minLat;
+    this.maxLat = maxLat;
+  }
+
+  @Override
+  public Weight createWeight(IndexSearcher searcher, boolean needsScores) throws IOException {
+
+    // I don't use RandomAccessWeight here: it's no good to approximate with "match all docs"; this is an inverted structure and should be
+    // used in the first pass:
+
+    // TODO: except that the polygon verify is costly!  The approximation should be all docs in all overlapping cells, and matches() should
+    // then check the polygon
+
+    return new ConstantScoreWeight(this) {
+
+      @Override
+      public Scorer scorer(LeafReaderContext context) throws IOException {
+        LeafReader reader = context.reader();
+        PointValues values = reader.getPointValues();
+        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);
+
+        DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc());
+        values.intersect(field,
+                         new IntersectVisitor() {
+                           @Override
+                           public void visit(int docID) {
+                             result.add(docID);
+                           }
+
+                           @Override
+                           public void visit(int docID, byte[] packedValue) {
+                             assert packedValue.length == 8;
+                             double lat = LatLonPoint.decodeLatitude(packedValue, 0);
+                             double lon = LatLonPoint.decodeLongitude(packedValue, Integer.BYTES);
+                             if (GeoRelationUtils.pointInPolygon(polyLons, polyLats, lat, lon)) {
+                               result.add(docID);
+                             }
+                           }
+
+                           @Override
+                           public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
+                             double cellMinLat = LatLonPoint.decodeLatitude(minPackedValue, 0);
+                             double cellMinLon = LatLonPoint.decodeLongitude(minPackedValue, Integer.BYTES);
+                             double cellMaxLat = LatLonPoint.decodeLatitude(maxPackedValue, 0);
+                             double cellMaxLon = LatLonPoint.decodeLongitude(maxPackedValue, Integer.BYTES);
+
+                             if (cellMinLat <= minLat && cellMaxLat >= maxLat && cellMinLon <= minLon && cellMaxLon >= maxLon) {
+                               // Cell fully encloses the query
+                               return Relation.CELL_CROSSES_QUERY;
+                             } else  if (GeoRelationUtils.rectWithinPolyPrecise(cellMinLon, cellMinLat, cellMaxLon, cellMaxLat,
+                                                                 polyLons, polyLats,
+                                                                 minLon, minLat, maxLon, maxLat)) {
+                               return Relation.CELL_INSIDE_QUERY;
+                             } else if (GeoRelationUtils.rectCrossesPolyPrecise(cellMinLon, cellMinLat, cellMaxLon, cellMaxLat,
+                                                                 polyLons, polyLats,
+                                                                 minLon, minLat, maxLon, maxLat)) {
+                               return Relation.CELL_CROSSES_QUERY;
+                             } else {
+                               return Relation.CELL_OUTSIDE_QUERY;
+                             }
+                           }
+                         });
+
+        return new ConstantScoreScorer(this, score(), result.build().iterator());
+      }
+    };
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    if (!super.equals(o)) return false;
+
+    LatLonPointInPolygonQuery that = (LatLonPointInPolygonQuery) o;
+
+    if (Arrays.equals(polyLons, that.polyLons) == false) {
+      return false;
+    }
+    if (Arrays.equals(polyLats, that.polyLats) == false) {
+      return false;
+    }
+
+    return true;
+  }
+
+  @Override
+  public final int hashCode() {
+    int result = super.hashCode();
+    result = 31 * result + Arrays.hashCode(polyLons);
+    result = 31 * result + Arrays.hashCode(polyLats);
+    return result;
+  }
+
+  @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(" Points: ");
+    for (int i=0; i<polyLons.length; ++i) {
+      sb.append("[")
+        .append(polyLons[i])
+        .append(", ")
+        .append(polyLats[i])
+        .append("] ");
+    }
+    return sb.toString();
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2264600f/lucene/sandbox/src/java/org/apache/lucene/document/package.html
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/package.html b/lucene/sandbox/src/java/org/apache/lucene/document/package.html
index 6f26128..b6a077e 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/document/package.html
+++ b/lucene/sandbox/src/java/org/apache/lucene/document/package.html
@@ -23,6 +23,11 @@
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
 </head>
 <body>
-This package contains a single Field implementation, GeoPointField, to index a lat/lon geospatial point.
+This package contains several point types:
+<ul>
+   <li>{@link org.apache.lucene.document.BigIntegerPoint BigIntegerPoint} for 128-bit integers</li>
+   <li>{@link org.apache.lucene.document.InetAddressPoint InetAddressPoint} for IPv4 and IPv6 network addresses</li>
+   <li>{@link org.apache.lucene.document.LatLonPoint LatLonPoint} for latitude/longitude geospatial points</li> 
+</ul>
 </body>
 </html>

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2264600f/lucene/sandbox/src/java/org/apache/lucene/search/PointDistanceQuery.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/PointDistanceQuery.java b/lucene/sandbox/src/java/org/apache/lucene/search/PointDistanceQuery.java
deleted file mode 100644
index 4cfeb2f..0000000
--- a/lucene/sandbox/src/java/org/apache/lucene/search/PointDistanceQuery.java
+++ /dev/null
@@ -1,179 +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.search;
-
-import java.io.IOException;
-
-import org.apache.lucene.document.LatLonPoint;
-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.spatial.util.GeoDistanceUtils;
-import org.apache.lucene.spatial.util.GeoRect;
-import org.apache.lucene.spatial.util.GeoUtils;
-import org.apache.lucene.util.DocIdSetBuilder;
-import org.apache.lucene.util.NumericUtils;
-
-/**
- * Distance query for {@link LatLonPoint}.
- */
-public class PointDistanceQuery extends Query {
-  final String field;
-  final double latitude;
-  final double longitude;
-  final double radiusMeters;
-
-  public PointDistanceQuery(String field, double latitude, double longitude, double radiusMeters) {
-    if (field == null) {
-      throw new IllegalArgumentException("field cannot be null");
-    }
-    if (GeoUtils.isValidLat(latitude) == false) {
-      throw new IllegalArgumentException("latitude: '" + latitude + "' is invalid");
-    }
-    if (GeoUtils.isValidLon(longitude) == false) {
-      throw new IllegalArgumentException("longitude: '" + longitude + "' is invalid");
-    }
-    this.field = field;
-    this.latitude = latitude;
-    this.longitude = longitude;
-    this.radiusMeters = radiusMeters;
-  }
-
-  @Override
-  public Weight createWeight(IndexSearcher searcher, boolean needsScores) throws IOException {
-    GeoRect box = GeoUtils.circleToBBox(longitude, latitude, radiusMeters);
-    final GeoRect box1;
-    final GeoRect box2;
-
-    // crosses dateline: split
-    if (box.maxLon < box.minLon) {
-      box1 = new GeoRect(-180.0, box.maxLon, box.minLat, box.maxLat);
-      box2 = new GeoRect(box.minLon, 180.0, box.minLat, box.maxLat);
-    } else {
-      box1 = box;
-      box2 = null;
-    }
-
-    return new ConstantScoreWeight(this) {
-
-      @Override
-      public Scorer scorer(LeafReaderContext context) throws IOException {
-        LeafReader reader = context.reader();
-        PointValues values = reader.getPointValues();
-        if (values == null) {
-          // No docs in this segment had any points fields
-          return null;
-        }
-        
-        DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc());
-        values.intersect(field,
-                         new IntersectVisitor() {
-                           @Override
-                           public void visit(int docID) {
-                             result.add(docID);
-                           }
-
-                           @Override
-                           public void visit(int docID, byte[] packedValue) {
-                             assert packedValue.length == 8;
-                             double lat = LatLonPoint.decodeLat(NumericUtils.bytesToInt(packedValue, 0));
-                             double lon = LatLonPoint.decodeLon(NumericUtils.bytesToInt(packedValue, Integer.BYTES));
-                             if (GeoDistanceUtils.haversin(latitude, longitude, lat, lon) <= radiusMeters) {
-                               visit(docID);
-                             }
-                           }
-                           
-                           // algorithm: we create a bounding box (two bounding boxes if we cross the dateline).
-                           // 1. check our bounding box(es) first. if the subtree is entirely outside of those, bail.
-                           // 2. see if the subtree is fully contained. if the subtree is enormous along the x axis, wrapping half way around the world, etc: then this can't work, just go to step 3.
-                           // 3. recurse naively.
-                           @Override
-                           public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
-                             double latMin = LatLonPoint.decodeLat(NumericUtils.bytesToInt(minPackedValue, 0));
-                             double lonMin = LatLonPoint.decodeLon(NumericUtils.bytesToInt(minPackedValue, Integer.BYTES));
-                             double latMax = LatLonPoint.decodeLat(NumericUtils.bytesToInt(maxPackedValue, 0));
-                             double lonMax = LatLonPoint.decodeLon(NumericUtils.bytesToInt(maxPackedValue, Integer.BYTES));
-                             
-                             if ((latMax < box1.minLat || lonMax < box1.minLon || latMin > box1.maxLat || lonMin > box1.maxLon) && 
-                                 (box2 == null || latMax < box2.minLat || lonMax < box2.minLon || latMin > box2.maxLat || lonMin > box2.maxLon)) {
-                               // we are fully outside of bounding box(es), don't proceed any further.
-                               return Relation.CELL_OUTSIDE_QUERY;
-                             } else if (lonMax - longitude < 90 && longitude - lonMin < 90 &&
-                                 GeoDistanceUtils.haversin(latitude, longitude, latMin, lonMin) <= radiusMeters &&
-                                 GeoDistanceUtils.haversin(latitude, longitude, latMin, lonMax) <= radiusMeters &&
-                                 GeoDistanceUtils.haversin(latitude, longitude, latMax, lonMin) <= radiusMeters &&
-                                 GeoDistanceUtils.haversin(latitude, longitude, latMax, lonMax) <= radiusMeters) {
-                               // we are fully enclosed, collect everything within this subtree
-                               return Relation.CELL_INSIDE_QUERY;
-                             } else {
-                               // recurse: its inside our bounding box(es), but not fully, or it wraps around.
-                               return Relation.CELL_CROSSES_QUERY;
-                             }
-                           }
-                         });
-
-        return new ConstantScoreScorer(this, score(), result.build().iterator());
-      }
-    };
-  }
-
-  @Override
-  public int hashCode() {
-    final int prime = 31;
-    int result = super.hashCode();
-    result = prime * result + field.hashCode();
-    long temp;
-    temp = Double.doubleToLongBits(latitude);
-    result = prime * result + (int) (temp ^ (temp >>> 32));
-    temp = Double.doubleToLongBits(longitude);
-    result = prime * result + (int) (temp ^ (temp >>> 32));
-    temp = Double.doubleToLongBits(radiusMeters);
-    result = prime * result + (int) (temp ^ (temp >>> 32));
-    return result;
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (this == obj) return true;
-    if (!super.equals(obj)) return false;
-    if (getClass() != obj.getClass()) return false;
-    PointDistanceQuery other = (PointDistanceQuery) obj;
-    if (!field.equals(other.field)) return false;
-    if (Double.doubleToLongBits(latitude) != Double.doubleToLongBits(other.latitude)) return false;
-    if (Double.doubleToLongBits(longitude) != Double.doubleToLongBits(other.longitude)) return false;
-    if (Double.doubleToLongBits(radiusMeters) != Double.doubleToLongBits(other.radiusMeters)) return false;
-    return true;
-  }
-
-  @Override
-  public String toString(String field) {
-    StringBuilder sb = new StringBuilder();
-    if (!this.field.equals(field)) {
-      sb.append(field);
-      sb.append(':');
-    }
-    sb.append(latitude);
-    sb.append(",");
-    sb.append(longitude);
-    sb.append(" +/- ");
-    sb.append(radiusMeters);
-    sb.append(" meters");
-    return sb.toString();
-  }
-}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2264600f/lucene/sandbox/src/java/org/apache/lucene/search/PointInPolygonQuery.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/PointInPolygonQuery.java b/lucene/sandbox/src/java/org/apache/lucene/search/PointInPolygonQuery.java
deleted file mode 100644
index 6ae7c03..0000000
--- a/lucene/sandbox/src/java/org/apache/lucene/search/PointInPolygonQuery.java
+++ /dev/null
@@ -1,207 +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.search;
-
-import java.io.IOException;
-import java.util.Arrays;
-
-import org.apache.lucene.document.LatLonPoint;
-import org.apache.lucene.index.PointValues.IntersectVisitor;
-import org.apache.lucene.index.PointValues.Relation;
-import org.apache.lucene.index.PointValues;
-import org.apache.lucene.index.LeafReader;
-import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.util.DocIdSetBuilder;
-import org.apache.lucene.util.NumericUtils;
-import org.apache.lucene.spatial.util.GeoRelationUtils;
-import org.apache.lucene.spatial.util.GeoUtils;
-
-/** Finds all previously indexed points that fall within the specified polygon.
- *
- *  <p>The field must be indexed with using {@link org.apache.lucene.document.LatLonPoint} added per document.
- *
- *  @lucene.experimental */
-
-public class PointInPolygonQuery extends Query {
-  final String field;
-  final double minLat;
-  final double maxLat;
-  final double minLon;
-  final double maxLon;
-  final double[] polyLats;
-  final double[] polyLons;
-
-  /** The lats/lons must be clockwise or counter-clockwise. */
-  public PointInPolygonQuery(String field, double[] polyLats, double[] polyLons) {
-    this.field = field;
-    if (polyLats.length != polyLons.length) {
-      throw new IllegalArgumentException("polyLats and polyLons must be equal length");
-    }
-    if (polyLats.length < 4) {
-      throw new IllegalArgumentException("at least 4 polygon points required");
-    }
-    if (polyLats[0] != polyLats[polyLats.length-1]) {
-      throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): polyLats[0]=" + polyLats[0] + " polyLats[" + (polyLats.length-1) + "]=" + polyLats[polyLats.length-1]);
-    }
-    if (polyLons[0] != polyLons[polyLons.length-1]) {
-      throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): polyLons[0]=" + polyLons[0] + " polyLons[" + (polyLons.length-1) + "]=" + polyLons[polyLons.length-1]);
-    }
-
-    this.polyLats = polyLats;
-    this.polyLons = polyLons;
-
-    // TODO: we could also compute the maximal inner bounding box, to make relations faster to compute?
-
-    double minLon = Double.POSITIVE_INFINITY;
-    double minLat = Double.POSITIVE_INFINITY;
-    double maxLon = Double.NEGATIVE_INFINITY;
-    double maxLat = Double.NEGATIVE_INFINITY;
-    for(int i=0;i<polyLats.length;i++) {
-      double lat = polyLats[i];
-      if (GeoUtils.isValidLat(lat) == false) {
-        throw new IllegalArgumentException("polyLats[" + i + "]=" + lat + " is not a valid latitude");
-      }
-      minLat = Math.min(minLat, lat);
-      maxLat = Math.max(maxLat, lat);
-      double lon = polyLons[i];
-      if (GeoUtils.isValidLon(lon) == false) {
-        throw new IllegalArgumentException("polyLons[" + i + "]=" + lat + " is not a valid longitude");
-      }
-      minLon = Math.min(minLon, lon);
-      maxLon = Math.max(maxLon, lon);
-    }
-    this.minLon = minLon;
-    this.maxLon = maxLon;
-    this.minLat = minLat;
-    this.maxLat = maxLat;
-  }
-
-  @Override
-  public Weight createWeight(IndexSearcher searcher, boolean needsScores) throws IOException {
-
-    // I don't use RandomAccessWeight here: it's no good to approximate with "match all docs"; this is an inverted structure and should be
-    // used in the first pass:
-
-    // TODO: except that the polygon verify is costly!  The approximation should be all docs in all overlapping cells, and matches() should
-    // then check the polygon
-
-    return new ConstantScoreWeight(this) {
-
-      @Override
-      public Scorer scorer(LeafReaderContext context) throws IOException {
-        LeafReader reader = context.reader();
-        PointValues values = reader.getPointValues();
-        if (values == null) {
-          // No docs in this segment had any points fields
-          return null;
-        }
-
-        DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc());
-        values.intersect(field,
-                         new IntersectVisitor() {
-                           @Override
-                           public void visit(int docID) {
-                             result.add(docID);
-                           }
-
-                           @Override
-                           public void visit(int docID, byte[] packedValue) {
-                             assert packedValue.length == 8;
-                             double lat = LatLonPoint.decodeLat(NumericUtils.bytesToInt(packedValue, 0));
-                             double lon = LatLonPoint.decodeLon(NumericUtils.bytesToInt(packedValue, Integer.BYTES));
-                             if (GeoRelationUtils.pointInPolygon(polyLons, polyLats, lat, lon)) {
-                               result.add(docID);
-                             }
-                           }
-
-                           @Override
-                           public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
-                             double cellMinLat = LatLonPoint.decodeLat(NumericUtils.bytesToInt(minPackedValue, 0));
-                             double cellMinLon = LatLonPoint.decodeLon(NumericUtils.bytesToInt(minPackedValue, Integer.BYTES));
-                             double cellMaxLat = LatLonPoint.decodeLat(NumericUtils.bytesToInt(maxPackedValue, 0));
-                             double cellMaxLon = LatLonPoint.decodeLon(NumericUtils.bytesToInt(maxPackedValue, Integer.BYTES));
-
-                             if (cellMinLat <= minLat && cellMaxLat >= maxLat && cellMinLon <= minLon && cellMaxLon >= maxLon) {
-                               // Cell fully encloses the query
-                               return Relation.CELL_CROSSES_QUERY;
-                             } else  if (GeoRelationUtils.rectWithinPolyPrecise(cellMinLon, cellMinLat, cellMaxLon, cellMaxLat,
-                                                                 polyLons, polyLats,
-                                                                 minLon, minLat, maxLon, maxLat)) {
-                               return Relation.CELL_INSIDE_QUERY;
-                             } else if (GeoRelationUtils.rectCrossesPolyPrecise(cellMinLon, cellMinLat, cellMaxLon, cellMaxLat,
-                                                                 polyLons, polyLats,
-                                                                 minLon, minLat, maxLon, maxLat)) {
-                               return Relation.CELL_CROSSES_QUERY;
-                             } else {
-                               return Relation.CELL_OUTSIDE_QUERY;
-                             }
-                           }
-                         });
-
-        return new ConstantScoreScorer(this, score(), result.build().iterator());
-      }
-    };
-  }
-
-  @Override
-  @SuppressWarnings({"unchecked","rawtypes"})
-  public boolean equals(Object o) {
-    if (this == o) return true;
-    if (o == null || getClass() != o.getClass()) return false;
-    if (!super.equals(o)) return false;
-
-    PointInPolygonQuery that = (PointInPolygonQuery) o;
-
-    if (Arrays.equals(polyLons, that.polyLons) == false) {
-      return false;
-    }
-    if (Arrays.equals(polyLats, that.polyLats) == false) {
-      return false;
-    }
-
-    return true;
-  }
-
-  @Override
-  public final int hashCode() {
-    int result = super.hashCode();
-    result = 31 * result + Arrays.hashCode(polyLons);
-    result = 31 * result + Arrays.hashCode(polyLats);
-    return result;
-  }
-
-  @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(" Points: ");
-    for (int i=0; i<polyLons.length; ++i) {
-      sb.append("[")
-        .append(polyLons[i])
-        .append(", ")
-        .append(polyLats[i])
-        .append("] ");
-    }
-    return sb.toString();
-  }
-}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2264600f/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPoint.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPoint.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPoint.java
index 34f4304..b67dec8 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPoint.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPoint.java
@@ -16,40 +16,22 @@
  */
 package org.apache.lucene.document;
 
-import java.io.IOException;
-import java.util.BitSet;
-
-import org.apache.lucene.codecs.FilterCodec;
-import org.apache.lucene.codecs.PointFormat;
-import org.apache.lucene.codecs.PointReader;
-import org.apache.lucene.codecs.PointWriter;
-import org.apache.lucene.codecs.lucene60.Lucene60PointReader;
-import org.apache.lucene.codecs.lucene60.Lucene60PointWriter;
 import org.apache.lucene.index.IndexReader;
-import org.apache.lucene.index.IndexWriterConfig;
 import org.apache.lucene.index.RandomIndexWriter;
-import org.apache.lucene.index.SegmentReadState;
-import org.apache.lucene.index.SegmentWriteState;
 import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.ScoreDoc;
-import org.apache.lucene.search.Sort;
-import org.apache.lucene.search.TopDocs;
-import org.apache.lucene.spatial.util.GeoDistanceUtils;
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.util.LuceneTestCase;
-import org.apache.lucene.util.TestUtil;
-import org.apache.lucene.util.bkd.BKDWriter;
 
 /** Simple tests for {@link LatLonPoint} */
 public class TestLatLonPoint extends LuceneTestCase {
 
-  /** Add a single address and search for it in a box */
+  /** Add a single point and search for it in a box */
   // NOTE: we don't currently supply an exact search, only ranges, because of the lossiness...
-  public void testBasics() throws Exception {
+  public void testBoxQuery() throws Exception {
     Directory dir = newDirectory();
     RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
 
-    // add a doc with an address
+    // add a doc with a point
     Document document = new Document();
     document.add(new LatLonPoint("field", 18.313694, -65.227444));
     writer.addDocument(document);
@@ -70,93 +52,122 @@ public class TestLatLonPoint extends LuceneTestCase {
     
     // looks crazy due to lossiness
     assertEquals("field:[17.99999997485429 TO 18.999999999068677},[-65.9999999217689 TO -64.99999998137355}", LatLonPoint.newBoxQuery("field", 18, 19, -66, -65).toString());
+    
+    // distance query does not quantize inputs
+    assertEquals("field:18.0,19.0 +/- 25.0 meters", LatLonPoint.newDistanceQuery("field", 18, 19, 25).toString());
   }
   
-  public void testRadiusRandom() throws Exception {
-    for (int iters = 0; iters < 100; iters++) {
-      doRandomTest(10, 100);
-    }
+  /** Valid values that should not cause exception */
+  public void testExtremeValues() {
+    new LatLonPoint("foo", 90.0, 180.0);
+    new LatLonPoint("foo", 90.0, -180.0);
+    new LatLonPoint("foo", -90.0, 180.0);
+    new LatLonPoint("foo", -90.0, -180.0);
   }
   
-  @Nightly
-  public void testRadiusRandomHuge() throws Exception {
-    for (int iters = 0; iters < 10; iters++) {
-      doRandomTest(2000, 100);
-    }
+  /** Invalid values */
+  public void testOutOfRangeValues() {
+    IllegalArgumentException expected;
+
+    expected = expectThrows(IllegalArgumentException.class, () -> {
+      new LatLonPoint("foo", Math.nextUp(90.0), 50.0);
+    });
+    assertTrue(expected.getMessage().contains("invalid latitude"));
+    
+    expected = expectThrows(IllegalArgumentException.class, () -> {
+      new LatLonPoint("foo", Math.nextDown(-90.0), 50.0);
+    });
+    assertTrue(expected.getMessage().contains("invalid latitude"));
+    
+    expected = expectThrows(IllegalArgumentException.class, () -> {
+      new LatLonPoint("foo", 90.0, Math.nextUp(180.0));
+    });
+    assertTrue(expected.getMessage().contains("invalid longitude"));
+    
+    expected = expectThrows(IllegalArgumentException.class, () -> {
+      new LatLonPoint("foo", 90.0, Math.nextDown(-180.0));
+    });
+    assertTrue(expected.getMessage().contains("invalid longitude"));
   }
   
-  private void doRandomTest(int numDocs, int numQueries) throws IOException {
-    Directory dir = newDirectory();
-    IndexWriterConfig iwc = newIndexWriterConfig();
-    int pointsInLeaf = 2 + random().nextInt(4);
-    iwc.setCodec(new FilterCodec("Lucene60", TestUtil.getDefaultCodec()) {
-      @Override
-      public PointFormat pointFormat() {
-        return new PointFormat() {
-          @Override
-          public PointWriter fieldsWriter(SegmentWriteState writeState) throws IOException {
-            return new Lucene60PointWriter(writeState, pointsInLeaf, BKDWriter.DEFAULT_MAX_MB_SORT_IN_HEAP);
-          }
+  /** NaN: illegal */
+  public void testNaNValues() {
+    IllegalArgumentException expected;
 
-          @Override
-          public PointReader fieldsReader(SegmentReadState readState) throws IOException {
-            return new Lucene60PointReader(readState);
-          }
-        };
-      }
+    expected = expectThrows(IllegalArgumentException.class, () -> {
+      new LatLonPoint("foo", Double.NaN, 50.0);
+    });
+    assertTrue(expected.getMessage().contains("invalid latitude"));
+    
+    expected = expectThrows(IllegalArgumentException.class, () -> {
+      new LatLonPoint("foo", 50.0, Double.NaN);
     });
-    RandomIndexWriter writer = new RandomIndexWriter(random(), dir, iwc);
+    assertTrue(expected.getMessage().contains("invalid longitude"));
+  }
+  
+  /** Inf: illegal */
+  public void testInfValues() {
+    IllegalArgumentException expected;
 
-    for (int i = 0; i < numDocs; i++) {
-      double latRaw = -90 + 180.0 * random().nextDouble();
-      double lonRaw = -180 + 360.0 * random().nextDouble();
-      // pre-normalize up front, so we can just use quantized value for testing and do simple exact comparisons
-      double lat = LatLonPoint.decodeLat(LatLonPoint.encodeLat(latRaw));
-      double lon = LatLonPoint.decodeLon(LatLonPoint.encodeLon(lonRaw));
-      Document doc = new Document();
-      doc.add(new LatLonPoint("field", lat, lon));
-      doc.add(new StoredField("lat", lat));
-      doc.add(new StoredField("lon", lon));
-      writer.addDocument(doc);
-    }
-    IndexReader reader = writer.getReader();
-    IndexSearcher searcher = new IndexSearcher(reader);
+    expected = expectThrows(IllegalArgumentException.class, () -> {
+      new LatLonPoint("foo", Double.POSITIVE_INFINITY, 50.0);
+    });
+    assertTrue(expected.getMessage().contains("invalid latitude"));
+    
+    expected = expectThrows(IllegalArgumentException.class, () -> {
+      new LatLonPoint("foo", Double.NEGATIVE_INFINITY, 50.0);
+    });
+    assertTrue(expected.getMessage().contains("invalid latitude"));
+    
+    expected = expectThrows(IllegalArgumentException.class, () -> {
+      new LatLonPoint("foo", 50.0, Double.POSITIVE_INFINITY);
+    });
+    assertTrue(expected.getMessage().contains("invalid longitude"));
+    
+    expected = expectThrows(IllegalArgumentException.class, () -> {
+      new LatLonPoint("foo", 50.0, Double.NEGATIVE_INFINITY);
+    });
+    assertTrue(expected.getMessage().contains("invalid longitude"));
+  }
+   
+  public void testEncodeDecode() throws Exception {
+    // just for testing quantization error
+    final double ENCODING_TOLERANCE = 1e-7;
 
-    for (int i = 0; i < numQueries; i++) {
+    int iters = atLeast(10000);
+    for(int iter=0;iter<iters;iter++) {
       double lat = -90 + 180.0 * random().nextDouble();
+      double latEnc = LatLonPoint.decodeLatitude(LatLonPoint.encodeLatitude(lat));
+      assertEquals("lat=" + lat + " latEnc=" + latEnc + " diff=" + (lat - latEnc), lat, latEnc, ENCODING_TOLERANCE);
+
       double lon = -180 + 360.0 * random().nextDouble();
-      double radius = 50000000 * random().nextDouble();
+      double lonEnc = LatLonPoint.decodeLongitude(LatLonPoint.encodeLongitude(lon));
+      assertEquals("lon=" + lon + " lonEnc=" + lonEnc + " diff=" + (lon - lonEnc), lon, lonEnc, ENCODING_TOLERANCE);
+    }
+
+    // check edge/interesting cases explicitly
+    assertEquals(0.0, LatLonPoint.decodeLatitude(LatLonPoint.encodeLatitude(0.0)), ENCODING_TOLERANCE);
+    assertEquals(90.0, LatLonPoint.decodeLatitude(LatLonPoint.encodeLatitude(90.0)), ENCODING_TOLERANCE);
+    assertEquals(-90.0, LatLonPoint.decodeLatitude(LatLonPoint.encodeLatitude(-90.0)), ENCODING_TOLERANCE);
 
-      BitSet expected = new BitSet();
-      for (int doc = 0; doc < reader.maxDoc(); doc++) {
-        double docLatitude = reader.document(doc).getField("lat").numericValue().doubleValue();
-        double docLongitude = reader.document(doc).getField("lon").numericValue().doubleValue();
-        double distance = GeoDistanceUtils.haversin(lat, lon, docLatitude, docLongitude);
-        if (distance <= radius) {
-          expected.set(doc);
-        }
-      }
+    assertEquals(0.0, LatLonPoint.decodeLongitude(LatLonPoint.encodeLongitude(0.0)), ENCODING_TOLERANCE);
+    assertEquals(180.0, LatLonPoint.decodeLongitude(LatLonPoint.encodeLongitude(180.0)), ENCODING_TOLERANCE);
+    assertEquals(-180.0, LatLonPoint.decodeLongitude(LatLonPoint.encodeLongitude(-180.0)), ENCODING_TOLERANCE);
+  }
 
-      TopDocs topDocs = searcher.search(LatLonPoint.newDistanceQuery("field", lat, lon, radius), reader.maxDoc(), Sort.INDEXORDER);
-      BitSet actual = new BitSet();
-      for (ScoreDoc doc : topDocs.scoreDocs) {
-        actual.set(doc.doc);
-      }
+  public void testEncodeDecodeIsStable() throws Exception {
+    int iters = atLeast(1000);
+    for(int iter=0;iter<iters;iter++) {
+      double lat = -90 + 180.0 * random().nextDouble();
+      double lon = -180 + 360.0 * random().nextDouble();
 
-      try {
-        assertEquals(expected, actual);
-      } catch (AssertionError e) {
-        for (int doc = 0; doc < reader.maxDoc(); doc++) {
-          double docLatitude = reader.document(doc).getField("lat").numericValue().doubleValue();
-          double docLongitude = reader.document(doc).getField("lon").numericValue().doubleValue();
-          double distance = GeoDistanceUtils.haversin(lat, lon, docLatitude, docLongitude);
-          System.out.println("" + doc + ": (" + docLatitude + "," + docLongitude + "), distance=" + distance);
-        }
-        throw e;
-      }
+      double latEnc = LatLonPoint.decodeLatitude(LatLonPoint.encodeLatitude(lat));
+      double lonEnc = LatLonPoint.decodeLongitude(LatLonPoint.encodeLongitude(lon));
+
+      double latEnc2 = LatLonPoint.decodeLatitude(LatLonPoint.encodeLatitude(latEnc));
+      double lonEnc2 = LatLonPoint.decodeLongitude(LatLonPoint.encodeLongitude(lonEnc));
+      assertEquals(latEnc, latEnc2, 0.0);
+      assertEquals(lonEnc, lonEnc2, 0.0);
     }
-    reader.close();
-    writer.close();
-    dir.close();
   }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2264600f/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointDistanceQuery.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointDistanceQuery.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointDistanceQuery.java
new file mode 100644
index 0000000..3d47b44
--- /dev/null
+++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointDistanceQuery.java
@@ -0,0 +1,190 @@
+/*
+ * 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.BitSet;
+
+import org.apache.lucene.codecs.FilterCodec;
+import org.apache.lucene.codecs.PointFormat;
+import org.apache.lucene.codecs.PointReader;
+import org.apache.lucene.codecs.PointWriter;
+import org.apache.lucene.codecs.lucene60.Lucene60PointReader;
+import org.apache.lucene.codecs.lucene60.Lucene60PointWriter;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.index.SegmentReadState;
+import org.apache.lucene.index.SegmentWriteState;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.spatial.util.GeoDistanceUtils;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.TestUtil;
+import org.apache.lucene.util.LuceneTestCase;
+import org.apache.lucene.util.bkd.BKDWriter;
+
+/** Simple tests for {@link LatLonPoint#newDistanceQuery} */
+public class TestLatLonPointDistanceQuery extends LuceneTestCase {
+
+  /** test we can search for a point */
+  public void testBasics() throws Exception {
+    Directory dir = newDirectory();
+    RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
+
+    // add a doc with a location
+    Document document = new Document();
+    document.add(new LatLonPoint("field", 18.313694, -65.227444));
+    writer.addDocument(document);
+    
+    // search within 50km and verify we found our doc
+    IndexReader reader = writer.getReader();
+    IndexSearcher searcher = newSearcher(reader);
+    assertEquals(1, searcher.count(LatLonPoint.newDistanceQuery("field", 18, -65, 50_000)));
+
+    reader.close();
+    writer.close();
+    dir.close();
+  }
+  
+  /** negative distance queries are not allowed */
+  public void testNegativeRadius() {
+    IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
+      LatLonPoint.newDistanceQuery("field", 18, 19, -1);
+    });
+    assertTrue(expected.getMessage().contains("radiusMeters"));
+    assertTrue(expected.getMessage().contains("is invalid"));
+  }
+  
+  /** NaN distance queries are not allowed */
+  public void testNaNRadius() {
+    IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
+      LatLonPoint.newDistanceQuery("field", 18, 19, Double.NaN);
+    });
+    assertTrue(expected.getMessage().contains("radiusMeters"));
+    assertTrue(expected.getMessage().contains("is invalid"));
+  }
+  
+  /** Inf distance queries are not allowed */
+  public void testInfRadius() {
+    IllegalArgumentException expected;
+    
+    expected = expectThrows(IllegalArgumentException.class, () -> {
+      LatLonPoint.newDistanceQuery("field", 18, 19, Double.POSITIVE_INFINITY);
+    });
+    assertTrue(expected.getMessage().contains("radiusMeters"));
+    assertTrue(expected.getMessage().contains("is invalid"));
+    
+    expected = expectThrows(IllegalArgumentException.class, () -> {
+      LatLonPoint.newDistanceQuery("field", 18, 19, Double.NEGATIVE_INFINITY);
+    });
+    assertTrue(expected.getMessage().contains("radiusMeters"));
+    assertTrue(expected.getMessage().contains("is invalid"));
+  }
+  
+  /** Run a few iterations with just 10 docs, hopefully easy to debug */
+  public void testRandom() throws Exception {
+    for (int iters = 0; iters < 100; iters++) {
+      doRandomTest(10, 100);
+    }
+  }
+  
+  /** Runs with thousands of docs */
+  @Nightly
+  public void testRandomHuge() throws Exception {
+    for (int iters = 0; iters < 10; iters++) {
+      doRandomTest(2000, 100);
+    }
+  }
+  
+  private void doRandomTest(int numDocs, int numQueries) throws IOException {
+    Directory dir = newDirectory();
+    IndexWriterConfig iwc = newIndexWriterConfig();
+    int pointsInLeaf = 2 + random().nextInt(4);
+    iwc.setCodec(new FilterCodec("Lucene60", TestUtil.getDefaultCodec()) {
+      @Override
+      public PointFormat pointFormat() {
+        return new PointFormat() {
+          @Override
+          public PointWriter fieldsWriter(SegmentWriteState writeState) throws IOException {
+            return new Lucene60PointWriter(writeState, pointsInLeaf, BKDWriter.DEFAULT_MAX_MB_SORT_IN_HEAP);
+          }
+
+          @Override
+          public PointReader fieldsReader(SegmentReadState readState) throws IOException {
+            return new Lucene60PointReader(readState);
+          }
+        };
+      }
+    });
+    RandomIndexWriter writer = new RandomIndexWriter(random(), dir, iwc);
+
+    for (int i = 0; i < numDocs; i++) {
+      double latRaw = -90 + 180.0 * random().nextDouble();
+      double lonRaw = -180 + 360.0 * random().nextDouble();
+      // pre-normalize up front, so we can just use quantized value for testing and do simple exact comparisons
+      double lat = LatLonPoint.decodeLatitude(LatLonPoint.encodeLatitude(latRaw));
+      double lon = LatLonPoint.decodeLongitude(LatLonPoint.encodeLongitude(lonRaw));
+      Document doc = new Document();
+      doc.add(new LatLonPoint("field", lat, lon));
+      doc.add(new StoredField("lat", lat));
+      doc.add(new StoredField("lon", lon));
+      writer.addDocument(doc);
+    }
+    IndexReader reader = writer.getReader();
+    IndexSearcher searcher = new IndexSearcher(reader);
+
+    for (int i = 0; i < numQueries; i++) {
+      double lat = -90 + 180.0 * random().nextDouble();
+      double lon = -180 + 360.0 * random().nextDouble();
+      double radius = 50000000 * random().nextDouble();
+
+      BitSet expected = new BitSet();
+      for (int doc = 0; doc < reader.maxDoc(); doc++) {
+        double docLatitude = reader.document(doc).getField("lat").numericValue().doubleValue();
+        double docLongitude = reader.document(doc).getField("lon").numericValue().doubleValue();
+        double distance = GeoDistanceUtils.haversin(lat, lon, docLatitude, docLongitude);
+        if (distance <= radius) {
+          expected.set(doc);
+        }
+      }
+
+      TopDocs topDocs = searcher.search(LatLonPoint.newDistanceQuery("field", lat, lon, radius), reader.maxDoc(), Sort.INDEXORDER);
+      BitSet actual = new BitSet();
+      for (ScoreDoc doc : topDocs.scoreDocs) {
+        actual.set(doc.doc);
+      }
+
+      try {
+        assertEquals(expected, actual);
+      } catch (AssertionError e) {
+        for (int doc = 0; doc < reader.maxDoc(); doc++) {
+          double docLatitude = reader.document(doc).getField("lat").numericValue().doubleValue();
+          double docLongitude = reader.document(doc).getField("lon").numericValue().doubleValue();
+          double distance = GeoDistanceUtils.haversin(lat, lon, docLatitude, docLongitude);
+          System.out.println("" + doc + ": (" + docLatitude + "," + docLongitude + "), distance=" + distance);
+        }
+        throw e;
+      }
+    }
+    reader.close();
+    writer.close();
+    dir.close();
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2264600f/lucene/sandbox/src/test/org/apache/lucene/search/TestLatLonPointQueries.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/search/TestLatLonPointQueries.java b/lucene/sandbox/src/test/org/apache/lucene/search/TestLatLonPointQueries.java
index 7798b97..8eb1be0 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/search/TestLatLonPointQueries.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/search/TestLatLonPointQueries.java
@@ -23,9 +23,8 @@ import org.apache.lucene.spatial.util.GeoDistanceUtils;
 import org.apache.lucene.spatial.util.GeoRect;
 
 public class TestLatLonPointQueries extends BaseGeoPointTestCase {
-  // todo deconflict GeoPoint and BKD encoding methods and error tolerance
+  // TODO: remove this!
   public static final double BKD_TOLERANCE = 1e-7;
-  public static final double ENCODING_TOLERANCE = 1e-7;
 
   @Override
   protected void addPointToDoc(String field, Document doc, double lat, double lon) {
@@ -57,13 +56,13 @@ public class TestLatLonPointQueries extends BaseGeoPointTestCase {
 
     assert Double.isNaN(pointLat) == false;
 
-    int rectLatMinEnc = LatLonPoint.encodeLat(rect.minLat);
-    int rectLatMaxEnc = LatLonPoint.encodeLat(rect.maxLat);
-    int rectLonMinEnc = LatLonPoint.encodeLon(rect.minLon);
-    int rectLonMaxEnc = LatLonPoint.encodeLon(rect.maxLon);
+    int rectLatMinEnc = LatLonPoint.encodeLatitude(rect.minLat);
+    int rectLatMaxEnc = LatLonPoint.encodeLatitude(rect.maxLat);
+    int rectLonMinEnc = LatLonPoint.encodeLongitude(rect.minLon);
+    int rectLonMaxEnc = LatLonPoint.encodeLongitude(rect.maxLon);
 
-    int pointLatEnc = LatLonPoint.encodeLat(pointLat);
-    int pointLonEnc = LatLonPoint.encodeLon(pointLon);
+    int pointLatEnc = LatLonPoint.encodeLatitude(pointLat);
+    int pointLonEnc = LatLonPoint.encodeLongitude(pointLon);
 
     if (rect.minLon < rect.maxLon) {
       return pointLatEnc >= rectLatMinEnc &&
@@ -81,12 +80,12 @@ public class TestLatLonPointQueries extends BaseGeoPointTestCase {
 
   @Override
   protected double quantizeLat(double latRaw) {
-    return LatLonPoint.decodeLat(LatLonPoint.encodeLat(latRaw));
+    return LatLonPoint.decodeLatitude(LatLonPoint.encodeLatitude(latRaw));
   }
 
   @Override
   protected double quantizeLon(double lonRaw) {
-    return LatLonPoint.decodeLon(LatLonPoint.encodeLon(lonRaw));
+    return LatLonPoint.decodeLongitude(LatLonPoint.encodeLongitude(lonRaw));
   }
 
   // todo reconcile with GeoUtils (see LUCENE-6996)
@@ -112,13 +111,13 @@ public class TestLatLonPointQueries extends BaseGeoPointTestCase {
       return null;
     }
 
-    int rectLatMinEnc = LatLonPoint.encodeLat(rect.minLat);
-    int rectLatMaxEnc = LatLonPoint.encodeLat(rect.maxLat);
-    int rectLonMinEnc = LatLonPoint.encodeLon(rect.minLon);
-    int rectLonMaxEnc = LatLonPoint.encodeLon(rect.maxLon);
+    int rectLatMinEnc = LatLonPoint.encodeLatitude(rect.minLat);
+    int rectLatMaxEnc = LatLonPoint.encodeLatitude(rect.maxLat);
+    int rectLonMinEnc = LatLonPoint.encodeLongitude(rect.minLon);
+    int rectLonMaxEnc = LatLonPoint.encodeLongitude(rect.maxLon);
 
-    int pointLatEnc = LatLonPoint.encodeLat(pointLat);
-    int pointLonEnc = LatLonPoint.encodeLon(pointLon);
+    int pointLatEnc = LatLonPoint.encodeLatitude(pointLat);
+    int pointLonEnc = LatLonPoint.encodeLongitude(pointLon);
 
     if (rect.minLon < rect.maxLon) {
       return pointLatEnc >= rectLatMinEnc &&
@@ -147,35 +146,4 @@ public class TestLatLonPointQueries extends BaseGeoPointTestCase {
     final double d = GeoDistanceUtils.haversin(centerLat, centerLon, pointLat, pointLon);
     return d >= minRadiusMeters && d <= radiusMeters;
   }
-
-  public void testEncodeDecode() throws Exception {
-    int iters = atLeast(10000);
-    boolean small = random().nextBoolean();
-    for(int iter=0;iter<iters;iter++) {
-      double lat = randomLat(small);
-      double latEnc = LatLonPoint.decodeLat(LatLonPoint.encodeLat(lat));
-      assertEquals("lat=" + lat + " latEnc=" + latEnc + " diff=" + (lat - latEnc), lat, latEnc, ENCODING_TOLERANCE);
-
-      double lon = randomLon(small);
-      double lonEnc = LatLonPoint.decodeLon(LatLonPoint.encodeLon(lon));
-      assertEquals("lon=" + lon + " lonEnc=" + lonEnc + " diff=" + (lon - lonEnc), lon, lonEnc, ENCODING_TOLERANCE);
-    }
-  }
-
-  public void testScaleUnscaleIsStable() throws Exception {
-    int iters = atLeast(1000);
-    boolean small = random().nextBoolean();
-    for(int iter=0;iter<iters;iter++) {
-      double lat = randomLat(small);
-      double lon = randomLon(small);
-
-      double latEnc = LatLonPoint.decodeLat(LatLonPoint.encodeLat(lat));
-      double lonEnc = LatLonPoint.decodeLon(LatLonPoint.encodeLon(lon));
-
-      double latEnc2 = LatLonPoint.decodeLat(LatLonPoint.encodeLat(latEnc));
-      double lonEnc2 = LatLonPoint.decodeLon(LatLonPoint.encodeLon(lonEnc));
-      assertEquals(latEnc, latEnc2, 0.0);
-      assertEquals(lonEnc, lonEnc2, 0.0);
-    }
-  }
 }