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

[84/87] [abbrv] lucene-solr git commit: LUCENE-6997: refactors lucene-spatial module to a new lucene-spatial-extras module, and refactors sandbox GeoPointField and queries to lucene-spatial module

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/sandbox/src/test/org/apache/lucene/util/TestGeoUtils.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/util/TestGeoUtils.java b/lucene/sandbox/src/test/org/apache/lucene/util/TestGeoUtils.java
deleted file mode 100644
index 5650614..0000000
--- a/lucene/sandbox/src/test/org/apache/lucene/util/TestGeoUtils.java
+++ /dev/null
@@ -1,545 +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.util;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import org.junit.BeforeClass;
-
-import com.carrotsearch.randomizedtesting.generators.RandomInts;
-
-import static org.apache.lucene.util.GeoDistanceUtils.DISTANCE_PCT_ERR;
-
-/**
- * Tests class for methods in GeoUtils
- *
- * @lucene.experimental
- */
-public class TestGeoUtils extends LuceneTestCase {
-
-  private static final double LON_SCALE = (0x1L<<GeoUtils.BITS)/360.0D;
-  private static final double LAT_SCALE = (0x1L<<GeoUtils.BITS)/180.0D;
-
-  // Global bounding box we will "cover" in the random test; we have to make this "smallish" else the queries take very long:
-  private static double originLat;
-  private static double originLon;
-  //  private static double range;
-  private static double lonRange;
-  private static double latRange;
-
-  @BeforeClass
-  public static void beforeClass() throws Exception {
-    // Between 1.0 and 3.0:
-    lonRange = 2 * (random().nextDouble() + 0.5);
-    latRange = 2 * (random().nextDouble() + 0.5);
-
-    originLon = GeoUtils.MIN_LON_INCL + lonRange + (GeoUtils.MAX_LON_INCL - GeoUtils.MIN_LON_INCL - 2 * lonRange) * random().nextDouble();
-    originLon = GeoUtils.normalizeLon(originLon);
-    originLat = GeoUtils.MIN_LAT_INCL + latRange + (GeoUtils.MAX_LAT_INCL - GeoUtils.MIN_LAT_INCL - 2 * latRange) * random().nextDouble();
-    originLat = GeoUtils.normalizeLat(originLat);
-
-    if (VERBOSE) {
-      System.out.println("TEST: originLon=" + originLon + " lonRange= " + lonRange + " originLat=" + originLat + " latRange=" + latRange);
-    }
-  }
-
-  public void testGeoHash() {
-    int numPoints = atLeast(100);
-    String randomGeoHashString;
-    String mortonGeoHash;
-    long mortonLongFromGHLong, geoHashLong, mortonLongFromGHString;
-    int randomLevel;
-    for (int i = 0; i < numPoints; ++i) {
-      // random point
-      double lat = randomLat(false);
-      double lon = randomLon(false);
-
-      // compute geohash straight from lat/lon and from morton encoded value to ensure they're the same
-      randomGeoHashString = GeoHashUtils.stringEncode(lon, lat, randomLevel = random().nextInt(12 - 1) + 1);
-      mortonGeoHash = GeoHashUtils.stringEncodeFromMortonLong(GeoUtils.mortonHash(lon, lat), randomLevel);
-      assertEquals(randomGeoHashString, mortonGeoHash);
-
-      // v&v conversion from lat/lon or geohashstring to geohash long and back to geohash string
-      geoHashLong = (random().nextBoolean()) ? GeoHashUtils.longEncode(lon, lat, randomLevel) : GeoHashUtils.longEncode(randomGeoHashString);
-      assertEquals(randomGeoHashString, GeoHashUtils.stringEncode(geoHashLong));
-
-      // v&v conversion from geohash long to morton long
-      mortonLongFromGHString = GeoHashUtils.mortonEncode(randomGeoHashString);
-      mortonLongFromGHLong = GeoHashUtils.mortonEncode(geoHashLong);
-      assertEquals(mortonLongFromGHLong, mortonLongFromGHString);
-
-      // v&v lat/lon from geohash string and geohash long
-      assertEquals(GeoUtils.mortonUnhashLat(mortonLongFromGHString), GeoUtils.mortonUnhashLat(mortonLongFromGHLong), 0);
-      assertEquals(GeoUtils.mortonUnhashLon(mortonLongFromGHString), GeoUtils.mortonUnhashLon(mortonLongFromGHLong), 0);
-    }
-  }
-
-  /**
-   * Pass condition: lat=42.6, lng=-5.6 should be encoded as "ezs42e44yx96",
-   * lat=57.64911 lng=10.40744 should be encoded as "u4pruydqqvj8"
-   */
-  public void testEncode() {
-    String hash = GeoHashUtils.stringEncode(-5.6, 42.6, 12);
-    assertEquals("ezs42e44yx96", hash);
-
-    hash = GeoHashUtils.stringEncode(10.40744, 57.64911, 12);
-    assertEquals("u4pruydqqvj8", hash);
-  }
-
-  /**
-   * Pass condition: lat=52.3738007, lng=4.8909347 should be encoded and then
-   * decoded within 0.00001 of the original value
-   */
-  public void testDecodePreciseLongitudeLatitude() {
-    final String geohash = GeoHashUtils.stringEncode(4.8909347, 52.3738007);
-    final long hash = GeoHashUtils.mortonEncode(geohash);
-
-    assertEquals(52.3738007, GeoUtils.mortonUnhashLat(hash), 0.00001D);
-    assertEquals(4.8909347, GeoUtils.mortonUnhashLon(hash), 0.00001D);
-  }
-
-  /**
-   * Pass condition: lat=84.6, lng=10.5 should be encoded and then decoded
-   * within 0.00001 of the original value
-   */
-  public void testDecodeImpreciseLongitudeLatitude() {
-    final String geohash = GeoHashUtils.stringEncode(10.5, 84.6);
-
-    final long hash = GeoHashUtils.mortonEncode(geohash);
-
-    assertEquals(84.6, GeoUtils.mortonUnhashLat(hash), 0.00001D);
-    assertEquals(10.5, GeoUtils.mortonUnhashLon(hash), 0.00001D);
-  }
-
-  public void testDecodeEncode() {
-    final String geoHash = "u173zq37x014";
-    assertEquals(geoHash, GeoHashUtils.stringEncode(4.8909347, 52.3738007));
-    final long mortonHash = GeoHashUtils.mortonEncode(geoHash);
-    final double lon = GeoUtils.mortonUnhashLon(mortonHash);
-    final double lat = GeoUtils.mortonUnhashLat(mortonHash);
-    assertEquals(52.37380061d, GeoUtils.mortonUnhashLat(mortonHash), 0.000001d);
-    assertEquals(4.8909343d, GeoUtils.mortonUnhashLon(mortonHash), 0.000001d);
-
-    assertEquals(geoHash, GeoHashUtils.stringEncode(lon, lat));
-  }
-
-  public void testNeighbors() {
-    String geohash = "gcpv";
-    List<String> expectedNeighbors = new ArrayList<>();
-    expectedNeighbors.add("gcpw");
-    expectedNeighbors.add("gcpy");
-    expectedNeighbors.add("u10n");
-    expectedNeighbors.add("gcpt");
-    expectedNeighbors.add("u10j");
-    expectedNeighbors.add("gcps");
-    expectedNeighbors.add("gcpu");
-    expectedNeighbors.add("u10h");
-    Collection<? super String> neighbors = new ArrayList<>();
-    GeoHashUtils.addNeighbors(geohash, neighbors );
-    assertEquals(expectedNeighbors, neighbors);
-
-    // Border odd geohash
-    geohash = "u09x";
-    expectedNeighbors = new ArrayList<>();
-    expectedNeighbors.add("u0c2");
-    expectedNeighbors.add("u0c8");
-    expectedNeighbors.add("u0cb");
-    expectedNeighbors.add("u09r");
-    expectedNeighbors.add("u09z");
-    expectedNeighbors.add("u09q");
-    expectedNeighbors.add("u09w");
-    expectedNeighbors.add("u09y");
-    neighbors = new ArrayList<>();
-    GeoHashUtils.addNeighbors(geohash, neighbors);
-    assertEquals(expectedNeighbors, neighbors);
-
-    // Border even geohash
-    geohash = "u09tv";
-    expectedNeighbors = new ArrayList<>();
-    expectedNeighbors.add("u09wh");
-    expectedNeighbors.add("u09wj");
-    expectedNeighbors.add("u09wn");
-    expectedNeighbors.add("u09tu");
-    expectedNeighbors.add("u09ty");
-    expectedNeighbors.add("u09ts");
-    expectedNeighbors.add("u09tt");
-    expectedNeighbors.add("u09tw");
-    neighbors = new ArrayList<>();
-    GeoHashUtils.addNeighbors(geohash, neighbors );
-    assertEquals(expectedNeighbors, neighbors);
-
-    // Border even and odd geohash
-    geohash = "ezzzz";
-    expectedNeighbors = new ArrayList<>();
-    expectedNeighbors.add("gbpbn");
-    expectedNeighbors.add("gbpbp");
-    expectedNeighbors.add("u0000");
-    expectedNeighbors.add("ezzzy");
-    expectedNeighbors.add("spbpb");
-    expectedNeighbors.add("ezzzw");
-    expectedNeighbors.add("ezzzx");
-    expectedNeighbors.add("spbp8");
-    neighbors = new ArrayList<>();
-    GeoHashUtils.addNeighbors(geohash, neighbors );
-    assertEquals(expectedNeighbors, neighbors);
-  }
-
-  public void testClosestPointOnBBox() {
-    double[] result = new double[2];
-    GeoDistanceUtils.closestPointOnBBox(20, 30, 40, 50, 70, 70, result);
-    assertEquals(40.0, result[0], 0.0);
-    assertEquals(50.0, result[1], 0.0);
-
-    GeoDistanceUtils.closestPointOnBBox(-20, -20, 0, 0, 70, 70, result);
-    assertEquals(0.0, result[0], 0.0);
-    assertEquals(0.0, result[1], 0.0);
-  }
-
-  private static class Cell {
-    static int nextCellID;
-
-    final Cell parent;
-    final int cellID;
-    final double minLon, maxLon;
-    final double minLat, maxLat;
-    final int splitCount;
-
-    public Cell(Cell parent,
-                double minLon, double minLat,
-                double maxLon, double maxLat,
-                int splitCount) {
-      assert maxLon >= minLon;
-      assert maxLat >= minLat;
-      this.parent = parent;
-      this.minLon = minLon;
-      this.minLat = minLat;
-      this.maxLon = maxLon;
-      this.maxLat = maxLat;
-      this.cellID = nextCellID++;
-      this.splitCount = splitCount;
-    }
-
-    /** Returns true if the quantized point lies within this cell, inclusive on all bounds. */
-    public boolean contains(double lon, double lat) {
-      return lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat;
-    }
-
-    @Override
-    public String toString() {
-      return "cell=" + cellID + (parent == null ? "" : " parentCellID=" + parent.cellID) + " lon: " + minLon + " TO " + maxLon + ", lat: " + minLat + " TO " + maxLat + ", splits: " + splitCount;
-    }
-  }
-
-  public long scaleLon(final double val) {
-    return (long) ((val-GeoUtils.MIN_LON_INCL) * LON_SCALE);
-  }
-
-  public long scaleLat(final double val) {
-    return (long) ((val-GeoUtils.MIN_LAT_INCL) * LAT_SCALE);
-  }
-
-  public double unscaleLon(final long val) {
-    return (val / LON_SCALE) + GeoUtils.MIN_LON_INCL;
-  }
-
-  public double unscaleLat(final long val) {
-    return (val / LAT_SCALE) + GeoUtils.MIN_LAT_INCL;
-  }
-
-  public double randomLat(boolean small) {
-    double result;
-    if (small) {
-      result = GeoUtils.normalizeLat(originLat + latRange * (random().nextDouble() - 0.5));
-    } else {
-      result = -90 + 180.0 * random().nextDouble();
-    }
-    return result;
-  }
-
-  public double randomLon(boolean small) {
-    double result;
-    if (small) {
-      result = GeoUtils.normalizeLon(originLon + lonRange * (random().nextDouble() - 0.5));
-    } else {
-      result = -180 + 360.0 * random().nextDouble();
-    }
-    return result;
-  }
-
-  private void findMatches(Set<Integer> hits, PrintWriter log, Cell root,
-                           double centerLon, double centerLat, double radiusMeters,
-                           double[] docLons, double[] docLats) {
-
-    if (VERBOSE) {
-      log.println("  root cell: " + root);
-    }
-
-    List<Cell> queue = new ArrayList<>();
-    queue.add(root);
-
-    int recurseDepth = RandomInts.randomIntBetween(random(), 5, 15);
-
-    while (queue.size() > 0) {
-      Cell cell = queue.get(queue.size()-1);
-      queue.remove(queue.size()-1);
-      if (VERBOSE) {
-        log.println("  cycle: " + cell + " queue.size()=" + queue.size());
-      }
-
-      if (random().nextInt(10) == 7 || cell.splitCount > recurseDepth) {
-        if (VERBOSE) {
-          log.println("    leaf");
-        }
-        // Leaf cell: brute force check all docs that fall within this cell:
-        for(int docID=0;docID<docLons.length;docID++) {
-          if (cell.contains(docLons[docID], docLats[docID])) {
-            double distanceMeters = GeoDistanceUtils.haversin(centerLat, centerLon, docLats[docID], docLons[docID]);
-            if (distanceMeters <= radiusMeters) {
-              if (VERBOSE) {
-                log.println("    check doc=" + docID + ": match!");
-              }
-              hits.add(docID);
-            } else {
-              if (VERBOSE) {
-                log.println("    check doc=" + docID + ": no match");
-              }
-            }
-          }
-        }
-      } else {
-
-        if (GeoRelationUtils.rectWithinCircle(cell.minLon, cell.minLat, cell.maxLon, cell.maxLat, centerLon, centerLat, radiusMeters)) {
-          // Query circle fully contains this cell, just addAll:
-          if (VERBOSE) {
-            log.println("    circle fully contains cell: now addAll");
-          }
-          for(int docID=0;docID<docLons.length;docID++) {
-            if (cell.contains(docLons[docID], docLats[docID])) {
-              if (VERBOSE) {
-                log.println("    addAll doc=" + docID);
-              }
-              hits.add(docID);
-            }
-          }
-          continue;
-        } else if (GeoRelationUtils.rectWithin(root.minLon, root.minLat, root.maxLon, root.maxLat,
-                                       cell.minLon, cell.minLat, cell.maxLon, cell.maxLat)) {
-          // Fall through below to "recurse"
-          if (VERBOSE) {
-            log.println("    cell fully contains circle: keep splitting");
-          }
-        } else if (GeoRelationUtils.rectCrossesCircle(cell.minLon, cell.minLat, cell.maxLon, cell.maxLat,
-                                              centerLon, centerLat, radiusMeters)) {
-          // Fall through below to "recurse"
-          if (VERBOSE) {
-            log.println("    cell overlaps circle: keep splitting");
-          }
-        } else {
-          if (VERBOSE) {
-            log.println("    no overlap: drop this cell");
-            for(int docID=0;docID<docLons.length;docID++) {
-              if (cell.contains(docLons[docID], docLats[docID])) {
-                if (VERBOSE) {
-                  log.println("    skip doc=" + docID);
-                }
-              }
-            }
-          }
-          continue;
-        }
-          
-        // Randomly split:
-        if (random().nextBoolean()) {
-
-          // Split on lon:
-          double splitValue = cell.minLon + (cell.maxLon - cell.minLon) * random().nextDouble();
-          if (VERBOSE) {
-            log.println("    now split on lon=" + splitValue);
-          }
-          Cell cell1 = new Cell(cell,
-                                cell.minLon, cell.minLat,
-                                splitValue, cell.maxLat,
-                                cell.splitCount+1);
-          Cell cell2 = new Cell(cell,
-                                splitValue, cell.minLat,
-                                cell.maxLon, cell.maxLat,
-                                cell.splitCount+1);
-          if (VERBOSE) {
-            log.println("    split cell1: " + cell1);
-            log.println("    split cell2: " + cell2);
-          }
-          queue.add(cell1);
-          queue.add(cell2);
-        } else {
-
-          // Split on lat:
-          double splitValue = cell.minLat + (cell.maxLat - cell.minLat) * random().nextDouble();
-          if (VERBOSE) {
-            log.println("    now split on lat=" + splitValue);
-          }
-          Cell cell1 = new Cell(cell,
-                                cell.minLon, cell.minLat,
-                                cell.maxLon, splitValue,
-                                cell.splitCount+1);
-          Cell cell2 = new Cell(cell,
-                                cell.minLon, splitValue,
-                                cell.maxLon, cell.maxLat,
-                                cell.splitCount+1);
-          if (VERBOSE) {
-            log.println("    split cells:\n      " + cell1 + "\n      " + cell2);
-          }
-          queue.add(cell1);
-          queue.add(cell2);
-        }
-      }
-    }
-  }
-
-  /** Tests consistency of GeoUtils.rectWithinCircle, .rectCrossesCircle, .rectWithin and SloppyMath.haversine distance check */
-  public void testGeoRelations() throws Exception {
-
-    int numDocs = atLeast(1000);
-    
-    boolean useSmallRanges = random().nextBoolean();
-
-    if (VERBOSE) {
-      System.out.println("TEST: " + numDocs + " docs useSmallRanges=" + useSmallRanges);
-    }
-
-    double[] docLons = new double[numDocs];
-    double[] docLats = new double[numDocs];
-    for(int docID=0;docID<numDocs;docID++) {
-      docLons[docID] = randomLon(useSmallRanges);
-      docLats[docID] = randomLat(useSmallRanges);
-      if (VERBOSE) {
-        System.out.println("  doc=" + docID + ": lon=" + docLons[docID] + " lat=" + docLats[docID]);
-      }
-    }
-
-    int iters = atLeast(10);
-
-    iters = atLeast(50);
-    
-    for(int iter=0;iter<iters;iter++) {
-
-      Cell.nextCellID = 0;
-
-      double centerLon = randomLon(useSmallRanges);
-      double centerLat = randomLat(useSmallRanges);
-
-      // So the circle covers at most 50% of the earth's surface:
-
-      double radiusMeters;
-
-      // TODO: large exotic rectangles created by BKD may be inaccurate up to 2 times DISTANCE_PCT_ERR.
-      // restricting size until LUCENE-6994 can be addressed
-      if (true || useSmallRanges) {
-        // Approx 3 degrees lon at the equator:
-        radiusMeters = random().nextDouble() * 333000;
-      } else {
-        radiusMeters = random().nextDouble() * GeoProjectionUtils.SEMIMAJOR_AXIS * Math.PI / 2.0;
-      }
-
-      StringWriter sw = new StringWriter();
-      PrintWriter log = new PrintWriter(sw, true);
-
-      if (VERBOSE) {
-        log.println("\nTEST: iter=" + iter + " radiusMeters=" + radiusMeters + " centerLon=" + centerLon + " centerLat=" + centerLat);
-      }
-
-      GeoRect bbox = GeoUtils.circleToBBox(centerLon, centerLat, radiusMeters);
-      
-      Set<Integer> hits = new HashSet<>();
-
-      if (bbox.maxLon < bbox.minLon) {
-        // Crosses dateline
-        log.println("  circle crosses dateline; first left query");
-        double unwrappedLon = centerLon;
-        if (unwrappedLon > bbox.maxLon) {
-          // unwrap left
-          unwrappedLon += -360.0D;
-        }
-        findMatches(hits, log,
-                    new Cell(null,
-                             -180, bbox.minLat,
-                             bbox.maxLon, bbox.maxLat,
-                             0),
-                    unwrappedLon, centerLat, radiusMeters, docLons, docLats);
-        log.println("  circle crosses dateline; now right query");
-        if (unwrappedLon < bbox.maxLon) {
-          // unwrap right
-          unwrappedLon += 360.0D;
-        }
-        findMatches(hits, log,
-                    new Cell(null,
-                             bbox.minLon, bbox.minLat,
-                             180, bbox.maxLat,
-                             0),
-                    unwrappedLon, centerLat, radiusMeters, docLons, docLats);
-      } else {
-        // Start with the root cell that fully contains the shape:
-        findMatches(hits, log,
-                    new Cell(null,
-                             bbox.minLon, bbox.minLat,
-                             bbox.maxLon, bbox.maxLat,
-                             0),
-                    centerLon, centerLat, radiusMeters,
-                    docLons, docLats);
-      }
-
-      if (VERBOSE) {
-        log.println("  " + hits.size() + " hits");
-      }
-
-      int failCount = 0;
-
-      // Done matching, now verify:
-      for(int docID=0;docID<numDocs;docID++) {
-        double distanceMeters = GeoDistanceUtils.haversin(centerLat, centerLon, docLats[docID], docLons[docID]);
-        final Boolean expected;
-        final double percentError = Math.abs(distanceMeters - radiusMeters) / distanceMeters;
-        if (percentError <= DISTANCE_PCT_ERR) {
-          expected = null;
-        } else {
-          expected = distanceMeters <= radiusMeters;
-        }
-
-        boolean actual = hits.contains(docID);
-        if (expected != null && actual != expected) {
-          if (actual) {
-            log.println("doc=" + docID + " matched but should not with distance error " + percentError + " on iteration " + iter);
-          } else {
-            log.println("doc=" + docID + " did not match but should with distance error " + percentError + " on iteration " + iter);
-          }
-          log.println("  lon=" + docLons[docID] + " lat=" + docLats[docID] + " distanceMeters=" + distanceMeters + " vs radiusMeters=" + radiusMeters);
-          failCount++;
-        }
-      }
-
-      if (failCount != 0) {
-        System.out.print(sw.toString());
-        fail(failCount + " incorrect hits (see above)");
-      }
-    }
-  }
-}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/build.xml
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/build.xml b/lucene/spatial-extras/build.xml
new file mode 100644
index 0000000..e1a1365
--- /dev/null
+++ b/lucene/spatial-extras/build.xml
@@ -0,0 +1,57 @@
+<?xml version="1.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.
+-->
+<project name="spatial-extras" default="default">
+  <description>
+    Geospatial search
+  </description>
+
+  <import file="../module-build.xml"/>
+
+  <path id="spatialextrasjar">
+     <fileset dir="lib"/>
+  </path>
+
+  <path id="classpath">
+    <path refid="base.classpath"/>
+    <path refid="spatialextrasjar"/>
+    <pathelement path="${queries.jar}" />
+    <pathelement path="${misc.jar}" />
+    <pathelement path="${spatial3d.jar}" />
+  </path>
+
+  <path id="test.classpath">
+    <path refid="test.base.classpath" />
+    <path refid="spatialextrasjar"/>
+    <pathelement path="src/test-files" />
+  </path>
+
+  <target name="compile-core" depends="jar-queries,jar-misc,jar-spatial3d,common.compile-core" />
+
+  <target name="javadocs" depends="javadocs-queries,javadocs-misc,javadocs-spatial3d,compile-core,check-javadocs-uptodate"
+          unless="javadocs-uptodate-${name}">
+    <invoke-module-javadoc>
+      <links>
+        <link href="../queries"/>
+        <link href="../misc"/>
+        <link href="../spatial3d"/>
+      </links>
+    </invoke-module-javadoc>
+  </target>
+</project>

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/ivy.xml
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/ivy.xml b/lucene/spatial-extras/ivy.xml
new file mode 100644
index 0000000..0229a9f
--- /dev/null
+++ b/lucene/spatial-extras/ivy.xml
@@ -0,0 +1,36 @@
+<!--
+   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.
+-->
+<ivy-module version="2.0"  xmlns:maven="http://ant.apache.org/ivy/maven">
+  <info organisation="org.apache.lucene" module="spatial-extras"/>
+  <configurations defaultconfmapping="compile->master;test->master">
+    <conf name="compile" transitive="false"/>
+    <conf name="test" transitive="false"/>
+  </configurations>
+  <dependencies>
+    <dependency org="com.spatial4j" name="spatial4j" rev="${/com.spatial4j/spatial4j}" conf="compile"/>
+
+    <dependency org="com.spatial4j" name="spatial4j" rev="${/com.spatial4j/spatial4j}" conf="test">
+      <artifact name="spatial4j" type="test" ext="jar" maven:classifier="tests" />
+    </dependency>
+
+    <dependency org="org.slf4j" name="slf4j-api" rev="${/org.slf4j/slf4j-api}" conf="test"/>
+
+    <exclude org="*" ext="*" matcher="regexp" type="${ivy.exclude.types}"/>
+  </dependencies>
+</ivy-module>

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/java/org/apache/lucene/spatial/SpatialStrategy.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/SpatialStrategy.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/SpatialStrategy.java
new file mode 100644
index 0000000..f433c11
--- /dev/null
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/SpatialStrategy.java
@@ -0,0 +1,149 @@
+/*
+ * 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.spatial;
+
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.shape.Point;
+import com.spatial4j.core.shape.Rectangle;
+import com.spatial4j.core.shape.Shape;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.queries.function.valuesource.ReciprocalFloatFunction;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.spatial.query.SpatialArgs;
+
+/**
+ * The SpatialStrategy encapsulates an approach to indexing and searching based
+ * on shapes.
+ * <p>
+ * Different implementations will support different features. A strategy should
+ * document these common elements:
+ * <ul>
+ *   <li>Can it index more than one shape per field?</li>
+ *   <li>What types of shapes can be indexed?</li>
+ *   <li>What types of query shapes can be used?</li>
+ *   <li>What types of query operations are supported?
+ *   This might vary per shape.</li>
+ *   <li>Does it use some type of cache?  When?
+ * </ul>
+ * If a strategy only supports certain shapes at index or query time, then in
+ * general it will throw an exception if given an incompatible one.  It will not
+ * be coerced into compatibility.
+ * <p>
+ * Note that a SpatialStrategy is not involved with the Lucene stored field
+ * values of shapes, which is immaterial to indexing and search.
+ * <p>
+ * Thread-safe.
+ * <p>
+ * This API is marked as experimental, however it is quite stable.
+ *
+ * @lucene.experimental
+ */
+public abstract class SpatialStrategy {
+
+  protected final SpatialContext ctx;
+  private final String fieldName;
+
+  /**
+   * Constructs the spatial strategy with its mandatory arguments.
+   */
+  public SpatialStrategy(SpatialContext ctx, String fieldName) {
+    if (ctx == null)
+      throw new IllegalArgumentException("ctx is required");
+    this.ctx = ctx;
+    if (fieldName == null || fieldName.length() == 0)
+      throw new IllegalArgumentException("fieldName is required");
+    this.fieldName = fieldName;
+  }
+
+  public SpatialContext getSpatialContext() {
+    return ctx;
+  }
+
+  /**
+   * The name of the field or the prefix of them if there are multiple
+   * fields needed internally.
+   * @return Not null.
+   */
+  public String getFieldName() {
+    return fieldName;
+  }
+
+  /**
+   * Returns the IndexableField(s) from the {@code shape} that are to be
+   * added to the {@link org.apache.lucene.document.Document}.  These fields
+   * are expected to be marked as indexed and not stored.
+   * <p>
+   * Note: If you want to <i>store</i> the shape as a string for retrieval in
+   * search results, you could add it like this:
+   * <pre>document.add(new StoredField(fieldName,ctx.toString(shape)));</pre>
+   * The particular string representation used doesn't matter to the Strategy
+   * since it doesn't use it.
+   *
+   * @return Not null nor will it have null elements.
+   * @throws UnsupportedOperationException if given a shape incompatible with the strategy
+   */
+  public abstract Field[] createIndexableFields(Shape shape);
+
+  /**
+   * See {@link #makeDistanceValueSource(com.spatial4j.core.shape.Point, double)} called with
+   * a multiplier of 1.0 (i.e. units of degrees).
+   */
+  public ValueSource makeDistanceValueSource(Point queryPoint) {
+    return makeDistanceValueSource(queryPoint, 1.0);
+  }
+
+  /**
+   * Make a ValueSource returning the distance between the center of the
+   * indexed shape and {@code queryPoint}.  If there are multiple indexed shapes
+   * then the closest one is chosen. The result is multiplied by {@code multiplier}, which
+   * conveniently is used to get the desired units.
+   */
+  public abstract ValueSource makeDistanceValueSource(Point queryPoint, double multiplier);
+
+  /**
+   * Make a Query based principally on {@link org.apache.lucene.spatial.query.SpatialOperation}
+   * and {@link Shape} from the supplied {@code args}.  It should be constant scoring of 1.
+   *
+   * @throws UnsupportedOperationException If the strategy does not support the shape in {@code args}
+   * @throws org.apache.lucene.spatial.query.UnsupportedSpatialOperation If the strategy does not support the {@link
+   * org.apache.lucene.spatial.query.SpatialOperation} in {@code args}.
+   */
+  public abstract Query makeQuery(SpatialArgs args);
+
+  /**
+   * Returns a ValueSource with values ranging from 1 to 0, depending inversely
+   * on the distance from {@link #makeDistanceValueSource(com.spatial4j.core.shape.Point,double)}.
+   * The formula is {@code c/(d + c)} where 'd' is the distance and 'c' is
+   * one tenth the distance to the farthest edge from the center. Thus the
+   * scores will be 1 for indexed points at the center of the query shape and as
+   * low as ~0.1 at its furthest edges.
+   */
+  public final ValueSource makeRecipDistanceValueSource(Shape queryShape) {
+    Rectangle bbox = queryShape.getBoundingBox();
+    double diagonalDist = ctx.getDistCalc().distance(
+        ctx.makePoint(bbox.getMinX(), bbox.getMinY()), bbox.getMaxX(), bbox.getMaxY());
+    double distToEdge = diagonalDist * 0.5;
+    float c = (float)distToEdge * 0.1f;//one tenth
+    return new ReciprocalFloatFunction(makeDistanceValueSource(queryShape.getCenter(), 1.0), 1f, c, c);
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName()+" field:"+fieldName+" ctx="+ctx;
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java
new file mode 100644
index 0000000..9d0afe1
--- /dev/null
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java
@@ -0,0 +1,251 @@
+/*
+ * 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.spatial.bbox;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.search.Explanation;
+
+import com.spatial4j.core.shape.Rectangle;
+
+/**
+ * The algorithm is implemented as envelope on envelope (rect on rect) overlays rather than
+ * complex polygon on complex polygon overlays.
+ * <p>
+ * Spatial relevance scoring algorithm:
+ * <DL>
+ *   <DT>queryArea</DT> <DD>the area of the input query envelope</DD>
+ *   <DT>targetArea</DT> <DD>the area of the target envelope (per Lucene document)</DD>
+ *   <DT>intersectionArea</DT> <DD>the area of the intersection between the query and target envelopes</DD>
+ *   <DT>queryTargetProportion</DT> <DD>A 0-1 factor that divides the score proportion between query and target.
+ *   0.5 is evenly.</DD>
+ *
+ *   <DT>queryRatio</DT> <DD>intersectionArea / queryArea; (see note)</DD>
+ *   <DT>targetRatio</DT> <DD>intersectionArea / targetArea; (see note)</DD>
+ *   <DT>queryFactor</DT> <DD>queryRatio * queryTargetProportion;</DD>
+ *   <DT>targetFactor</DT> <DD>targetRatio * (1 - queryTargetProportion);</DD>
+ *   <DT>score</DT> <DD>queryFactor + targetFactor;</DD>
+ * </DL>
+ * Additionally, note that an optional minimum side length {@code minSideLength} may be used whenever an
+ * area is calculated (queryArea, targetArea, intersectionArea). This allows for points or horizontal/vertical lines
+ * to be used as the query shape and in such case the descending order should have smallest boxes up front. Without
+ * this, a point or line query shape typically scores everything with the same value since there is 0 area.
+ * <p>
+ * Note: The actual computation of queryRatio and targetRatio is more complicated so that it considers
+ * points and lines. Lines have the ratio of overlap, and points are either 1.0 or 0.0 depending on whether
+ * it intersects or not.
+ * <p>
+ * Originally based on Geoportal's
+ * <a href="http://geoportal.svn.sourceforge.net/svnroot/geoportal/Geoportal/trunk/src/com/esri/gpt/catalog/lucene/SpatialRankingValueSource.java">
+ *   SpatialRankingValueSource</a> but modified quite a bit. GeoPortal's algorithm will yield a score of 0
+ * if either a line or point is compared, and it doesn't output a 0-1 normalized score (it multiplies the factors),
+ * and it doesn't support minSideLength, and it had dateline bugs.
+ *
+ * @lucene.experimental
+ */
+public class BBoxOverlapRatioValueSource extends BBoxSimilarityValueSource {
+
+  private final boolean isGeo;//-180/+180 degrees  (not part of identity; attached to parent strategy/field)
+
+  private final Rectangle queryExtent;
+  private final double queryArea;//not part of identity
+
+  private final double minSideLength;
+
+  private final double queryTargetProportion;
+
+  //TODO option to compute geodetic area
+
+  /**
+   *
+   * @param rectValueSource mandatory; source of rectangles
+   * @param isGeo True if ctx.isGeo() and thus dateline issues should be attended to
+   * @param queryExtent mandatory; the query rectangle
+   * @param queryTargetProportion see class javadocs. Between 0 and 1.
+   * @param minSideLength see class javadocs. 0.0 will effectively disable.
+   */
+  public BBoxOverlapRatioValueSource(ValueSource rectValueSource, boolean isGeo, Rectangle queryExtent,
+                                     double queryTargetProportion, double minSideLength) {
+    super(rectValueSource);
+    this.isGeo = isGeo;
+    this.minSideLength = minSideLength;
+    this.queryExtent = queryExtent;
+    this.queryArea = calcArea(queryExtent.getWidth(), queryExtent.getHeight());
+    assert queryArea >= 0;
+    this.queryTargetProportion = queryTargetProportion;
+    if (queryTargetProportion < 0 || queryTargetProportion > 1.0)
+      throw new IllegalArgumentException("queryTargetProportion must be >= 0 and <= 1");
+  }
+
+  /** Construct with 75% weighting towards target (roughly GeoPortal's default), geo degrees assumed, no
+   * minimum side length. */
+  public BBoxOverlapRatioValueSource(ValueSource rectValueSource, Rectangle queryExtent) {
+    this(rectValueSource, true, queryExtent, 0.25, 0.0);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!super.equals(o)) return false;
+
+    BBoxOverlapRatioValueSource that = (BBoxOverlapRatioValueSource) o;
+
+    if (Double.compare(that.minSideLength, minSideLength) != 0) return false;
+    if (Double.compare(that.queryTargetProportion, queryTargetProportion) != 0) return false;
+    if (!queryExtent.equals(that.queryExtent)) return false;
+
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = super.hashCode();
+    long temp;
+    result = 31 * result + queryExtent.hashCode();
+    temp = Double.doubleToLongBits(minSideLength);
+    result = 31 * result + (int) (temp ^ (temp >>> 32));
+    temp = Double.doubleToLongBits(queryTargetProportion);
+    result = 31 * result + (int) (temp ^ (temp >>> 32));
+    return result;
+  }
+
+  @Override
+  protected String similarityDescription() {
+    return queryExtent.toString() + "," + queryTargetProportion;
+  }
+
+  @Override
+  protected double score(Rectangle target, AtomicReference<Explanation> exp) {
+    // calculate "height": the intersection height between two boxes.
+    double top = Math.min(queryExtent.getMaxY(), target.getMaxY());
+    double bottom = Math.max(queryExtent.getMinY(), target.getMinY());
+    double height = top - bottom;
+    if (height < 0) {
+      if (exp != null) {
+        exp.set(Explanation.noMatch("No intersection"));
+      }
+      return 0;//no intersection
+    }
+
+    // calculate "width": the intersection width between two boxes.
+    double width = 0;
+    {
+      Rectangle a = queryExtent;
+      Rectangle b = target;
+      if (a.getCrossesDateLine() == b.getCrossesDateLine()) {
+        //both either cross or don't
+        double left = Math.max(a.getMinX(), b.getMinX());
+        double right = Math.min(a.getMaxX(), b.getMaxX());
+        if (!a.getCrossesDateLine()) {//both don't
+          if (left <= right) {
+            width = right - left;
+          } else if (isGeo && (Math.abs(a.getMinX()) == 180 || Math.abs(a.getMaxX()) == 180)
+              && (Math.abs(b.getMinX()) == 180 || Math.abs(b.getMaxX()) == 180)) {
+            width = 0;//both adjacent to dateline
+          } else {
+            if (exp != null) {
+              exp.set(Explanation.noMatch("No intersection"));
+            }
+            return 0;//no intersection
+          }
+        } else {//both cross
+          width = right - left + 360;
+        }
+      } else {
+        if (!a.getCrossesDateLine()) {//then flip
+          a = target;
+          b = queryExtent;
+        }
+        //a crosses, b doesn't
+        double qryWestLeft = Math.max(a.getMinX(), b.getMinX());
+        double qryWestRight = b.getMaxX();
+        if (qryWestLeft < qryWestRight)
+          width += qryWestRight - qryWestLeft;
+
+        double qryEastLeft = b.getMinX();
+        double qryEastRight = Math.min(a.getMaxX(), b.getMaxX());
+        if (qryEastLeft < qryEastRight)
+          width += qryEastRight - qryEastLeft;
+
+        if (qryWestLeft > qryWestRight && qryEastLeft > qryEastRight) {
+          if (exp != null) {
+            exp.set(Explanation.noMatch("No intersection"));
+          }
+          return 0;//no intersection
+        }
+      }
+    }
+
+    // calculate queryRatio and targetRatio
+    double intersectionArea = calcArea(width, height);
+    double queryRatio;
+    if (queryArea > 0) {
+      queryRatio = intersectionArea / queryArea;
+    } else if (queryExtent.getHeight() > 0) {//vert line
+      queryRatio = height / queryExtent.getHeight();
+    } else if (queryExtent.getWidth() > 0) {//horiz line
+      queryRatio = width / queryExtent.getWidth();
+    } else {
+      queryRatio = queryExtent.relate(target).intersects() ? 1 : 0;//could be optimized
+    }
+
+    double targetArea = calcArea(target.getWidth(), target.getHeight());
+    assert targetArea >= 0;
+    double targetRatio;
+    if (targetArea > 0) {
+      targetRatio = intersectionArea / targetArea;
+    } else if (target.getHeight() > 0) {//vert line
+      targetRatio = height / target.getHeight();
+    } else if (target.getWidth() > 0) {//horiz line
+      targetRatio = width / target.getWidth();
+    } else {
+      targetRatio = target.relate(queryExtent).intersects() ? 1 : 0;//could be optimized
+    }
+    assert queryRatio >= 0 && queryRatio <= 1 : queryRatio;
+    assert targetRatio >= 0 && targetRatio <= 1 : targetRatio;
+
+    // combine ratios into a score
+
+    double queryFactor = queryRatio * queryTargetProportion;
+    double targetFactor = targetRatio * (1.0 - queryTargetProportion);
+    double score = queryFactor + targetFactor;
+
+    if (exp!=null) {
+      String minSideDesc = minSideLength > 0.0 ? " (minSide="+minSideLength+")" : "";
+      exp.set(Explanation.match((float) score,
+          this.getClass().getSimpleName()+": queryFactor + targetFactor",
+          Explanation.match((float)intersectionArea, "IntersectionArea" + minSideDesc,
+              Explanation.match((float)width, "width"),
+              Explanation.match((float)height, "height"),
+              Explanation.match((float)queryTargetProportion, "queryTargetProportion")),
+          Explanation.match((float)queryFactor, "queryFactor",
+              Explanation.match((float)targetRatio, "ratio"),
+              Explanation.match((float)queryArea,  "area of " + queryExtent + minSideDesc)),
+          Explanation.match((float)targetFactor, "targetFactor",
+              Explanation.match((float)targetRatio, "ratio"),
+              Explanation.match((float)targetArea,  "area of " + target + minSideDesc))));
+    }
+
+    return score;
+  }
+
+  /** Calculates the area while applying the minimum side length. */
+  private double calcArea(double width, double height) {
+    return Math.max(minSideLength, width) * Math.max(minSideLength, height);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxSimilarityValueSource.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxSimilarityValueSource.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxSimilarityValueSource.java
new file mode 100644
index 0000000..15cd646
--- /dev/null
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxSimilarityValueSource.java
@@ -0,0 +1,117 @@
+/*
+ * 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.spatial.bbox;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.queries.function.FunctionValues;
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.queries.function.docvalues.DoubleDocValues;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.IndexSearcher;
+
+import com.spatial4j.core.shape.Rectangle;
+
+/**
+ * A base class for calculating a spatial relevance rank per document from a provided
+ * {@link ValueSource} in which {@link FunctionValues#objectVal(int)} returns a {@link
+ * com.spatial4j.core.shape.Rectangle}.
+ * <p>
+ * Implementers: remember to implement equals and hashCode if you have
+ * fields!
+ *
+ * @lucene.experimental
+ */
+public abstract class BBoxSimilarityValueSource extends ValueSource {
+
+  private final ValueSource bboxValueSource;
+
+  public BBoxSimilarityValueSource(ValueSource bboxValueSource) {
+    this.bboxValueSource = bboxValueSource;
+  }
+
+  @Override
+  public void createWeight(Map context, IndexSearcher searcher) throws IOException {
+    bboxValueSource.createWeight(context, searcher);
+  }
+
+  @Override
+  public String description() {
+    return getClass().getSimpleName()+"(" + bboxValueSource.description() + "," + similarityDescription() + ")";
+  }
+
+  /** A comma-separated list of configurable items of the subclass to put into {@link #description()}. */
+  protected abstract String similarityDescription();
+
+  @Override
+  public FunctionValues getValues(Map context, LeafReaderContext readerContext) throws IOException {
+
+    final FunctionValues shapeValues = bboxValueSource.getValues(context, readerContext);
+
+    return new DoubleDocValues(this) {
+      @Override
+      public double doubleVal(int doc) {
+        //? limit to Rect or call getBoundingBox()? latter would encourage bad practice
+        final Rectangle rect = (Rectangle) shapeValues.objectVal(doc);
+        return rect==null ? 0 : score(rect, null);
+      }
+
+      @Override
+      public boolean exists(int doc) {
+        return shapeValues.exists(doc);
+      }
+
+      @Override
+      public Explanation explain(int doc) {
+        final Rectangle rect = (Rectangle) shapeValues.objectVal(doc);
+        if (rect == null)
+          return Explanation.noMatch("no rect");
+        AtomicReference<Explanation> explanation = new AtomicReference<>();
+        score(rect, explanation);
+        return explanation.get();
+      }
+    };
+  }
+
+  /**
+   * Return a relevancy score. If {@code exp} is provided then diagnostic information is added.
+   * @param rect The indexed rectangle; not null.
+   * @param exp Optional diagnostic holder.
+   * @return a score.
+   */
+  protected abstract double score(Rectangle rect, AtomicReference<Explanation> exp);
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;//same class
+
+    BBoxSimilarityValueSource that = (BBoxSimilarityValueSource) o;
+
+    if (!bboxValueSource.equals(that.bboxValueSource)) return false;
+
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    return bboxValueSource.hashCode();
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java
new file mode 100644
index 0000000..d5f7747
--- /dev/null
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java
@@ -0,0 +1,588 @@
+/*
+ * 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.spatial.bbox;
+
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.shape.Point;
+import com.spatial4j.core.shape.Rectangle;
+import com.spatial4j.core.shape.Shape;
+import org.apache.lucene.document.LegacyDoubleField;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FieldType;
+import org.apache.lucene.document.StringField;
+import org.apache.lucene.index.DocValuesType;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.ConstantScoreQuery;
+import org.apache.lucene.search.LegacyNumericRangeQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.spatial.SpatialStrategy;
+import org.apache.lucene.spatial.query.SpatialArgs;
+import org.apache.lucene.spatial.query.SpatialOperation;
+import org.apache.lucene.spatial.query.UnsupportedSpatialOperation;
+import org.apache.lucene.spatial.util.DistanceToShapeValueSource;
+import org.apache.lucene.util.BytesRefBuilder;
+import org.apache.lucene.util.LegacyNumericUtils;
+
+
+/**
+ * A SpatialStrategy for indexing and searching Rectangles by storing its
+ * coordinates in numeric fields. It supports all {@link SpatialOperation}s and
+ * has a custom overlap relevancy. It is based on GeoPortal's <a
+ * href="http://geoportal.svn.sourceforge.net/svnroot/geoportal/Geoportal/trunk/src/com/esri/gpt/catalog/lucene/SpatialClauseAdapter.java">SpatialClauseAdapter</a>.
+ * <p>
+ * <b>Characteristics:</b>
+ * <br>
+ * <ul>
+ * <li>Only indexes Rectangles; just one per field value. Other shapes can be provided
+ * and the bounding box will be used.</li>
+ * <li>Can query only by a Rectangle. Providing other shapes is an error.</li>
+ * <li>Supports most {@link SpatialOperation}s but not Overlaps.</li>
+ * <li>Uses the DocValues API for any sorting / relevancy.</li>
+ * </ul>
+ * <p>
+ * <b>Implementation:</b>
+ * <p>
+ * This uses 4 double fields for minX, maxX, minY, maxY
+ * and a boolean to mark a dateline cross. Depending on the particular {@link
+ * SpatialOperation}s, there are a variety of {@link org.apache.lucene.search.LegacyNumericRangeQuery}s to be
+ * done.
+ * The {@link #makeOverlapRatioValueSource(com.spatial4j.core.shape.Rectangle, double)}
+ * works by calculating the query bbox overlap percentage against the indexed
+ * shape overlap percentage. The indexed shape's coordinates are retrieved from
+ * {@link org.apache.lucene.index.LeafReader#getNumericDocValues}.
+ *
+ * @lucene.experimental
+ */
+public class BBoxStrategy extends SpatialStrategy {
+
+  public static final String SUFFIX_MINX = "__minX";
+  public static final String SUFFIX_MAXX = "__maxX";
+  public static final String SUFFIX_MINY = "__minY";
+  public static final String SUFFIX_MAXY = "__maxY";
+  public static final String SUFFIX_XDL  = "__xdl";
+
+  /*
+   * The Bounding Box gets stored as four fields for x/y min/max and a flag
+   * that says if the box crosses the dateline (xdl).
+   */
+  protected final String field_bbox;
+  protected final String field_minX;
+  protected final String field_minY;
+  protected final String field_maxX;
+  protected final String field_maxY;
+  protected final String field_xdl; // crosses dateline
+
+  protected FieldType fieldType;//for the 4 numbers
+  protected FieldType xdlFieldType;
+
+  public BBoxStrategy(SpatialContext ctx, String fieldNamePrefix) {
+    super(ctx, fieldNamePrefix);
+    field_bbox = fieldNamePrefix;
+    field_minX = fieldNamePrefix + SUFFIX_MINX;
+    field_maxX = fieldNamePrefix + SUFFIX_MAXX;
+    field_minY = fieldNamePrefix + SUFFIX_MINY;
+    field_maxY = fieldNamePrefix + SUFFIX_MAXY;
+    field_xdl = fieldNamePrefix + SUFFIX_XDL;
+
+    FieldType fieldType = new FieldType(LegacyDoubleField.TYPE_NOT_STORED);
+    fieldType.setNumericPrecisionStep(8);//Solr's default
+    fieldType.setDocValuesType(DocValuesType.NUMERIC);
+    setFieldType(fieldType);
+  }
+
+  private int getPrecisionStep() {
+    return fieldType.numericPrecisionStep();
+  }
+
+  public FieldType getFieldType() {
+    return fieldType;
+  }
+
+  /** Used to customize the indexing options of the 4 number fields, and to a lesser degree the XDL field too. Search
+   * requires indexed=true, and relevancy requires docValues. If these features aren't needed then disable them.
+   * {@link FieldType#freeze()} is called on the argument. */
+  public void setFieldType(FieldType fieldType) {
+    fieldType.freeze();
+    this.fieldType = fieldType;
+    //only double's supported right now
+    if (fieldType.numericType() != FieldType.LegacyNumericType.DOUBLE)
+      throw new IllegalArgumentException("BBoxStrategy only supports doubles at this time.");
+    //for xdlFieldType, copy some similar options. Don't do docValues since it isn't needed here.
+    xdlFieldType = new FieldType(StringField.TYPE_NOT_STORED);
+    xdlFieldType.setStored(fieldType.stored());
+    xdlFieldType.setIndexOptions(fieldType.indexOptions());
+    xdlFieldType.freeze();
+  }
+
+  //---------------------------------
+  // Indexing
+  //---------------------------------
+
+  @Override
+  public Field[] createIndexableFields(Shape shape) {
+    return createIndexableFields(shape.getBoundingBox());
+  }
+
+  public Field[] createIndexableFields(Rectangle bbox) {
+    Field[] fields = new Field[5];
+    fields[0] = new ComboField(field_minX, bbox.getMinX(), fieldType);
+    fields[1] = new ComboField(field_maxX, bbox.getMaxX(), fieldType);
+    fields[2] = new ComboField(field_minY, bbox.getMinY(), fieldType);
+    fields[3] = new ComboField(field_maxY, bbox.getMaxY(), fieldType);
+    fields[4] = new ComboField(field_xdl, bbox.getCrossesDateLine()?"T":"F", xdlFieldType);
+    return fields;
+  }
+
+  /** Field subclass circumventing Field limitations. This one instance can have any combination of indexed, stored,
+   * and docValues.
+   */
+  private static class ComboField extends Field {
+    private ComboField(String name, Object value, FieldType type) {
+      super(name, type);//this expert constructor allows us to have a field that has docValues & indexed/stored
+      super.fieldsData = value;
+    }
+
+    //Is this a hack?  We assume that numericValue() is only called for DocValues purposes.
+    @Override
+    public Number numericValue() {
+      //Numeric DocValues only supports Long,
+      final Number number = super.numericValue();
+      if (number == null)
+        return null;
+      if (fieldType().numericType() == FieldType.LegacyNumericType.DOUBLE)
+        return Double.doubleToLongBits(number.doubleValue());
+      if (fieldType().numericType() == FieldType.LegacyNumericType.FLOAT)
+        return Float.floatToIntBits(number.floatValue());
+      return number.longValue();
+    }
+  }
+
+  //---------------------------------
+  // Value Source / Relevancy
+  //---------------------------------
+
+  /**
+   * Provides access to each rectangle per document as a ValueSource in which
+   * {@link org.apache.lucene.queries.function.FunctionValues#objectVal(int)} returns a {@link
+   * Shape}.
+   */ //TODO raise to SpatialStrategy
+  public ValueSource makeShapeValueSource() {
+    return new BBoxValueSource(this);
+  }
+
+  @Override
+  public ValueSource makeDistanceValueSource(Point queryPoint, double multiplier) {
+    //TODO if makeShapeValueSource gets lifted to the top; this could become a generic impl.
+    return new DistanceToShapeValueSource(makeShapeValueSource(), queryPoint, multiplier, ctx);
+  }
+
+  /** Returns a similarity based on {@link BBoxOverlapRatioValueSource}. This is just a
+   * convenience method. */
+  public ValueSource makeOverlapRatioValueSource(Rectangle queryBox, double queryTargetProportion) {
+    return new BBoxOverlapRatioValueSource(
+        makeShapeValueSource(), ctx.isGeo(), queryBox, queryTargetProportion, 0.0);
+  }
+
+  //---------------------------------
+  // Query Building
+  //---------------------------------
+
+  //  Utility on SpatialStrategy?
+//  public Query makeQueryWithValueSource(SpatialArgs args, ValueSource valueSource) {
+//    return new CustomScoreQuery(makeQuery(args), new FunctionQuery(valueSource));
+  //or...
+//  return new BooleanQuery.Builder()
+//      .add(new FunctionQuery(valueSource), BooleanClause.Occur.MUST)//matches everything and provides score
+//      .add(filterQuery, BooleanClause.Occur.FILTER)//filters (score isn't used)
+//  .build();
+//  }
+
+  @Override
+  public Query makeQuery(SpatialArgs args) {
+    Shape shape = args.getShape();
+    if (!(shape instanceof Rectangle))
+      throw new UnsupportedOperationException("Can only query by Rectangle, not " + shape);
+
+    Rectangle bbox = (Rectangle) shape;
+    Query spatial;
+
+    // Useful for understanding Relations:
+    // http://edndoc.esri.com/arcsde/9.1/general_topics/understand_spatial_relations.htm
+    SpatialOperation op = args.getOperation();
+         if( op == SpatialOperation.BBoxIntersects ) spatial = makeIntersects(bbox);
+    else if( op == SpatialOperation.BBoxWithin     ) spatial = makeWithin(bbox);
+    else if( op == SpatialOperation.Contains       ) spatial = makeContains(bbox);
+    else if( op == SpatialOperation.Intersects     ) spatial = makeIntersects(bbox);
+    else if( op == SpatialOperation.IsEqualTo      ) spatial = makeEquals(bbox);
+    else if( op == SpatialOperation.IsDisjointTo   ) spatial = makeDisjoint(bbox);
+    else if( op == SpatialOperation.IsWithin       ) spatial = makeWithin(bbox);
+    else { //no Overlaps support yet
+        throw new UnsupportedSpatialOperation(op);
+    }
+    return new ConstantScoreQuery(spatial);
+  }
+
+  /**
+   * Constructs a query to retrieve documents that fully contain the input envelope.
+   *
+   * @return the spatial query
+   */
+  Query makeContains(Rectangle bbox) {
+
+    // general case
+    // docMinX <= queryExtent.getMinX() AND docMinY <= queryExtent.getMinY() AND docMaxX >= queryExtent.getMaxX() AND docMaxY >= queryExtent.getMaxY()
+
+    // Y conditions
+    // docMinY <= queryExtent.getMinY() AND docMaxY >= queryExtent.getMaxY()
+    Query qMinY = LegacyNumericRangeQuery.newDoubleRange(field_minY, getPrecisionStep(), null, bbox.getMinY(), false, true);
+    Query qMaxY = LegacyNumericRangeQuery.newDoubleRange(field_maxY, getPrecisionStep(), bbox.getMaxY(), null, true, false);
+    Query yConditions = this.makeQuery(BooleanClause.Occur.MUST, qMinY, qMaxY);
+
+    // X conditions
+    Query xConditions;
+
+    // queries that do not cross the date line
+    if (!bbox.getCrossesDateLine()) {
+
+      // X Conditions for documents that do not cross the date line,
+      // documents that contain the min X and max X of the query envelope,
+      // docMinX <= queryExtent.getMinX() AND docMaxX >= queryExtent.getMaxX()
+      Query qMinX = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), null, bbox.getMinX(), false, true);
+      Query qMaxX = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), bbox.getMaxX(), null, true, false);
+      Query qMinMax = this.makeQuery(BooleanClause.Occur.MUST, qMinX, qMaxX);
+      Query qNonXDL = this.makeXDL(false, qMinMax);
+
+      if (!ctx.isGeo()) {
+        xConditions = qNonXDL;
+      } else {
+        // X Conditions for documents that cross the date line,
+        // the left portion of the document contains the min X of the query
+        // OR the right portion of the document contains the max X of the query,
+        // docMinXLeft <= queryExtent.getMinX() OR docMaxXRight >= queryExtent.getMaxX()
+        Query qXDLLeft = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), null, bbox.getMinX(), false, true);
+        Query qXDLRight = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), bbox.getMaxX(), null, true, false);
+        Query qXDLLeftRight = this.makeQuery(BooleanClause.Occur.SHOULD, qXDLLeft, qXDLRight);
+        Query qXDL = this.makeXDL(true, qXDLLeftRight);
+
+        Query qEdgeDL = null;
+        if (bbox.getMinX() == bbox.getMaxX() && Math.abs(bbox.getMinX()) == 180) {
+          double edge = bbox.getMinX() * -1;//opposite dateline edge
+          qEdgeDL = makeQuery(BooleanClause.Occur.SHOULD,
+              makeNumberTermQuery(field_minX, edge), makeNumberTermQuery(field_maxX, edge));
+        }
+
+        // apply the non-XDL and XDL conditions
+        xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL, qEdgeDL);
+      }
+    } else {
+      // queries that cross the date line
+
+      // No need to search for documents that do not cross the date line
+
+      // X Conditions for documents that cross the date line,
+      // the left portion of the document contains the min X of the query
+      // AND the right portion of the document contains the max X of the query,
+      // docMinXLeft <= queryExtent.getMinX() AND docMaxXRight >= queryExtent.getMaxX()
+      Query qXDLLeft = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), null, bbox.getMinX(), false, true);
+      Query qXDLRight = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), bbox.getMaxX(), null, true, false);
+      Query qXDLLeftRight = this.makeXDL(true, this.makeQuery(BooleanClause.Occur.MUST, qXDLLeft, qXDLRight));
+
+      Query qWorld = makeQuery(BooleanClause.Occur.MUST,
+          makeNumberTermQuery(field_minX, -180), makeNumberTermQuery(field_maxX, 180));
+
+      xConditions = makeQuery(BooleanClause.Occur.SHOULD, qXDLLeftRight, qWorld);
+    }
+
+    // both X and Y conditions must occur
+    return this.makeQuery(BooleanClause.Occur.MUST, xConditions, yConditions);
+  }
+
+  /**
+   * Constructs a query to retrieve documents that are disjoint to the input envelope.
+   *
+   * @return the spatial query
+   */
+  Query makeDisjoint(Rectangle bbox) {
+
+    // general case
+    // docMinX > queryExtent.getMaxX() OR docMaxX < queryExtent.getMinX() OR docMinY > queryExtent.getMaxY() OR docMaxY < queryExtent.getMinY()
+
+    // Y conditions
+    // docMinY > queryExtent.getMaxY() OR docMaxY < queryExtent.getMinY()
+    Query qMinY = LegacyNumericRangeQuery.newDoubleRange(field_minY, getPrecisionStep(), bbox.getMaxY(), null, false, false);
+    Query qMaxY = LegacyNumericRangeQuery.newDoubleRange(field_maxY, getPrecisionStep(), null, bbox.getMinY(), false, false);
+    Query yConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qMinY, qMaxY);
+
+    // X conditions
+    Query xConditions;
+
+    // queries that do not cross the date line
+    if (!bbox.getCrossesDateLine()) {
+
+      // X Conditions for documents that do not cross the date line,
+      // docMinX > queryExtent.getMaxX() OR docMaxX < queryExtent.getMinX()
+      Query qMinX = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMaxX(), null, false, false);
+      if (bbox.getMinX() == -180.0 && ctx.isGeo()) {//touches dateline; -180 == 180
+        BooleanQuery.Builder bq = new BooleanQuery.Builder();
+        bq.add(qMinX, BooleanClause.Occur.MUST);
+        bq.add(makeNumberTermQuery(field_maxX, 180.0), BooleanClause.Occur.MUST_NOT);
+        qMinX = bq.build();
+      }
+      Query qMaxX = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMinX(), false, false);
+      if (bbox.getMaxX() == 180.0 && ctx.isGeo()) {//touches dateline; -180 == 180
+        BooleanQuery.Builder bq = new BooleanQuery.Builder();
+        bq.add(qMaxX, BooleanClause.Occur.MUST);
+        bq.add(makeNumberTermQuery(field_minX, -180.0), BooleanClause.Occur.MUST_NOT);
+        qMaxX = bq.build();
+      }
+      Query qMinMax = this.makeQuery(BooleanClause.Occur.SHOULD, qMinX, qMaxX);
+      Query qNonXDL = this.makeXDL(false, qMinMax);
+
+      if (!ctx.isGeo()) {
+        xConditions = qNonXDL;
+      } else {
+        // X Conditions for documents that cross the date line,
+
+        // both the left and right portions of the document must be disjoint to the query
+        // (docMinXLeft > queryExtent.getMaxX() OR docMaxXLeft < queryExtent.getMinX()) AND
+        // (docMinXRight > queryExtent.getMaxX() OR docMaxXRight < queryExtent.getMinX())
+        // where: docMaxXLeft = 180.0, docMinXRight = -180.0
+        // (docMaxXLeft  < queryExtent.getMinX()) equates to (180.0  < queryExtent.getMinX()) and is ignored
+        // (docMinXRight > queryExtent.getMaxX()) equates to (-180.0 > queryExtent.getMaxX()) and is ignored
+        Query qMinXLeft = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMaxX(), null, false, false);
+        Query qMaxXRight = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMinX(), false, false);
+        Query qLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qMinXLeft, qMaxXRight);
+        Query qXDL = this.makeXDL(true, qLeftRight);
+
+        // apply the non-XDL and XDL conditions
+        xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL);
+      }
+      // queries that cross the date line
+    } else {
+
+      // X Conditions for documents that do not cross the date line,
+      // the document must be disjoint to both the left and right query portions
+      // (docMinX > queryExtent.getMaxX()Left OR docMaxX < queryExtent.getMinX()) AND (docMinX > queryExtent.getMaxX() OR docMaxX < queryExtent.getMinX()Left)
+      // where: queryExtent.getMaxX()Left = 180.0, queryExtent.getMinX()Left = -180.0
+      Query qMinXLeft = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), 180.0, null, false, false);
+      Query qMaxXLeft = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMinX(), false, false);
+      Query qMinXRight = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMaxX(), null, false, false);
+      Query qMaxXRight = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, -180.0, false, false);
+      Query qLeft = this.makeQuery(BooleanClause.Occur.SHOULD, qMinXLeft, qMaxXLeft);
+      Query qRight = this.makeQuery(BooleanClause.Occur.SHOULD, qMinXRight, qMaxXRight);
+      Query qLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qLeft, qRight);
+
+      // No need to search for documents that do not cross the date line
+
+      xConditions = this.makeXDL(false, qLeftRight);
+    }
+
+    // either X or Y conditions should occur
+    return this.makeQuery(BooleanClause.Occur.SHOULD, xConditions, yConditions);
+  }
+
+  /**
+   * Constructs a query to retrieve documents that equal the input envelope.
+   *
+   * @return the spatial query
+   */
+  Query makeEquals(Rectangle bbox) {
+
+    // docMinX = queryExtent.getMinX() AND docMinY = queryExtent.getMinY() AND docMaxX = queryExtent.getMaxX() AND docMaxY = queryExtent.getMaxY()
+    Query qMinX = makeNumberTermQuery(field_minX, bbox.getMinX());
+    Query qMinY = makeNumberTermQuery(field_minY, bbox.getMinY());
+    Query qMaxX = makeNumberTermQuery(field_maxX, bbox.getMaxX());
+    Query qMaxY = makeNumberTermQuery(field_maxY, bbox.getMaxY());
+    return makeQuery(BooleanClause.Occur.MUST, qMinX, qMinY, qMaxX, qMaxY);
+  }
+
+  /**
+   * Constructs a query to retrieve documents that intersect the input envelope.
+   *
+   * @return the spatial query
+   */
+  Query makeIntersects(Rectangle bbox) {
+
+    // the original intersects query does not work for envelopes that cross the date line,
+    // switch to a NOT Disjoint query
+
+    // MUST_NOT causes a problem when it's the only clause type within a BooleanQuery,
+    // to get around it we add all documents as a SHOULD
+
+    // there must be an envelope, it must not be disjoint
+    Query qHasEnv;
+    if (ctx.isGeo()) {
+      Query qIsNonXDL = this.makeXDL(false);
+      Query qIsXDL = ctx.isGeo() ? this.makeXDL(true) : null;
+      qHasEnv = this.makeQuery(BooleanClause.Occur.SHOULD, qIsNonXDL, qIsXDL);
+    } else {
+      qHasEnv = this.makeXDL(false);
+    }
+
+    BooleanQuery.Builder qNotDisjoint = new BooleanQuery.Builder();
+    qNotDisjoint.add(qHasEnv, BooleanClause.Occur.MUST);
+    Query qDisjoint = makeDisjoint(bbox);
+    qNotDisjoint.add(qDisjoint, BooleanClause.Occur.MUST_NOT);
+
+    //Query qDisjoint = makeDisjoint();
+    //BooleanQuery qNotDisjoint = new BooleanQuery();
+    //qNotDisjoint.add(new MatchAllDocsQuery(),BooleanClause.Occur.SHOULD);
+    //qNotDisjoint.add(qDisjoint,BooleanClause.Occur.MUST_NOT);
+    return qNotDisjoint.build();
+  }
+
+  /**
+   * Makes a boolean query based upon a collection of queries and a logical operator.
+   *
+   * @param occur the logical operator
+   * @param queries the query collection
+   * @return the query
+   */
+  BooleanQuery makeQuery(BooleanClause.Occur occur, Query... queries) {
+    BooleanQuery.Builder bq = new BooleanQuery.Builder();
+    for (Query query : queries) {
+      if (query != null)
+        bq.add(query, occur);
+    }
+    return bq.build();
+  }
+
+  /**
+   * Constructs a query to retrieve documents are fully within the input envelope.
+   *
+   * @return the spatial query
+   */
+  Query makeWithin(Rectangle bbox) {
+
+    // general case
+    // docMinX >= queryExtent.getMinX() AND docMinY >= queryExtent.getMinY() AND docMaxX <= queryExtent.getMaxX() AND docMaxY <= queryExtent.getMaxY()
+
+    // Y conditions
+    // docMinY >= queryExtent.getMinY() AND docMaxY <= queryExtent.getMaxY()
+    Query qMinY = LegacyNumericRangeQuery.newDoubleRange(field_minY, getPrecisionStep(), bbox.getMinY(), null, true, false);
+    Query qMaxY = LegacyNumericRangeQuery.newDoubleRange(field_maxY, getPrecisionStep(), null, bbox.getMaxY(), false, true);
+    Query yConditions = this.makeQuery(BooleanClause.Occur.MUST, qMinY, qMaxY);
+
+    // X conditions
+    Query xConditions;
+
+    if (ctx.isGeo() && bbox.getMinX() == -180.0 && bbox.getMaxX() == 180.0) {
+      //if query world-wraps, only the y condition matters
+      return yConditions;
+
+    } else if (!bbox.getCrossesDateLine()) {
+      // queries that do not cross the date line
+
+      // docMinX >= queryExtent.getMinX() AND docMaxX <= queryExtent.getMaxX()
+      Query qMinX = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMinX(), null, true, false);
+      Query qMaxX = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMaxX(), false, true);
+      Query qMinMax = this.makeQuery(BooleanClause.Occur.MUST, qMinX, qMaxX);
+
+      double edge = 0;//none, otherwise opposite dateline of query
+      if (bbox.getMinX() == -180.0)
+        edge = 180;
+      else if (bbox.getMaxX() == 180.0)
+        edge = -180;
+      if (edge != 0 && ctx.isGeo()) {
+        Query edgeQ = makeQuery(BooleanClause.Occur.MUST,
+            makeNumberTermQuery(field_minX, edge), makeNumberTermQuery(field_maxX, edge));
+        qMinMax = makeQuery(BooleanClause.Occur.SHOULD, qMinMax, edgeQ);
+      }
+
+      xConditions = this.makeXDL(false, qMinMax);
+
+      // queries that cross the date line
+    } else {
+
+      // X Conditions for documents that do not cross the date line
+
+      // the document should be within the left portion of the query
+      // docMinX >= queryExtent.getMinX() AND docMaxX <= 180.0
+      Query qMinXLeft = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMinX(), null, true, false);
+      Query qMaxXLeft = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, 180.0, false, true);
+      Query qLeft = this.makeQuery(BooleanClause.Occur.MUST, qMinXLeft, qMaxXLeft);
+
+      // the document should be within the right portion of the query
+      // docMinX >= -180.0 AND docMaxX <= queryExtent.getMaxX()
+      Query qMinXRight = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), -180.0, null, true, false);
+      Query qMaxXRight = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMaxX(), false, true);
+      Query qRight = this.makeQuery(BooleanClause.Occur.MUST, qMinXRight, qMaxXRight);
+
+      // either left or right conditions should occur,
+      // apply the left and right conditions to documents that do not cross the date line
+      Query qLeftRight = this.makeQuery(BooleanClause.Occur.SHOULD, qLeft, qRight);
+      Query qNonXDL = this.makeXDL(false, qLeftRight);
+
+      // X Conditions for documents that cross the date line,
+      // the left portion of the document must be within the left portion of the query,
+      // AND the right portion of the document must be within the right portion of the query
+      // docMinXLeft >= queryExtent.getMinX() AND docMaxXLeft <= 180.0
+      // AND docMinXRight >= -180.0 AND docMaxXRight <= queryExtent.getMaxX()
+      Query qXDLLeft = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMinX(), null, true, false);
+      Query qXDLRight = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMaxX(), false, true);
+      Query qXDLLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qXDLLeft, qXDLRight);
+      Query qXDL = this.makeXDL(true, qXDLLeftRight);
+
+      // apply the non-XDL and XDL conditions
+      xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL);
+    }
+
+    // both X and Y conditions must occur
+    return this.makeQuery(BooleanClause.Occur.MUST, xConditions, yConditions);
+  }
+
+  /**
+   * Constructs a query to retrieve documents that do or do not cross the date line.
+   *
+   * @param crossedDateLine <code>true</true> for documents that cross the date line
+   * @return the query
+   */
+  private Query makeXDL(boolean crossedDateLine) {
+    // The 'T' and 'F' values match solr fields
+    return new TermQuery(new Term(field_xdl, crossedDateLine ? "T" : "F"));
+  }
+
+  /**
+   * Constructs a query to retrieve documents that do or do not cross the date line
+   * and match the supplied spatial query.
+   *
+   * @param crossedDateLine <code>true</true> for documents that cross the date line
+   * @param query the spatial query
+   * @return the query
+   */
+  private Query makeXDL(boolean crossedDateLine, Query query) {
+    if (!ctx.isGeo()) {
+      assert !crossedDateLine;
+      return query;
+    }
+    BooleanQuery.Builder bq = new BooleanQuery.Builder();
+    bq.add(this.makeXDL(crossedDateLine), BooleanClause.Occur.MUST);
+    bq.add(query, BooleanClause.Occur.MUST);
+    return bq.build();
+  }
+
+  private Query makeNumberTermQuery(String field, double number) {
+    BytesRefBuilder bytes = new BytesRefBuilder();
+    LegacyNumericUtils.longToPrefixCodedBytes(LegacyNumericUtils.doubleToSortableLong(number), 0, bytes);
+    return new TermQuery(new Term(field, bytes.get()));
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxValueSource.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxValueSource.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxValueSource.java
new file mode 100644
index 0000000..5d95407
--- /dev/null
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxValueSource.java
@@ -0,0 +1,115 @@
+/*
+ * 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.spatial.bbox;
+
+import com.spatial4j.core.shape.Rectangle;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.DocValues;
+import org.apache.lucene.index.NumericDocValues;
+import org.apache.lucene.queries.function.FunctionValues;
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.util.Bits;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * A ValueSource in which the indexed Rectangle is returned from
+ * {@link org.apache.lucene.queries.function.FunctionValues#objectVal(int)}.
+ *
+ * @lucene.internal
+ */
+class BBoxValueSource extends ValueSource {
+
+  private final BBoxStrategy strategy;
+
+  public BBoxValueSource(BBoxStrategy strategy) {
+    this.strategy = strategy;
+  }
+
+  @Override
+  public String description() {
+    return "bboxShape(" + strategy.getFieldName() + ")";
+  }
+
+  @Override
+  public FunctionValues getValues(Map context, LeafReaderContext readerContext) throws IOException {
+    LeafReader reader = readerContext.reader();
+    final NumericDocValues minX = DocValues.getNumeric(reader, strategy.field_minX);
+    final NumericDocValues minY = DocValues.getNumeric(reader, strategy.field_minY);
+    final NumericDocValues maxX = DocValues.getNumeric(reader, strategy.field_maxX);
+    final NumericDocValues maxY = DocValues.getNumeric(reader, strategy.field_maxY);
+
+    final Bits validBits = DocValues.getDocsWithField(reader, strategy.field_minX);//could have chosen any field
+    //reused
+    final Rectangle rect = strategy.getSpatialContext().makeRectangle(0,0,0,0);
+
+    return new FunctionValues() {
+      @Override
+      public Object objectVal(int doc) {
+        if (!validBits.get(doc)) {
+          return null;
+        } else {
+          rect.reset(
+              Double.longBitsToDouble(minX.get(doc)), Double.longBitsToDouble(maxX.get(doc)),
+              Double.longBitsToDouble(minY.get(doc)), Double.longBitsToDouble(maxY.get(doc)));
+          return rect;
+        }
+      }
+
+      @Override
+      public String strVal(int doc) {//TODO support WKT output once Spatial4j does
+        Object v = objectVal(doc);
+        return v == null ? null : v.toString();
+      }
+
+      @Override
+      public boolean exists(int doc) {
+        return validBits.get(doc);
+      }
+
+      @Override
+      public Explanation explain(int doc) {
+        return Explanation.match(Float.NaN, toString(doc));
+      }
+
+      @Override
+      public String toString(int doc) {
+        return description() + '=' + strVal(doc);
+      }
+    };
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    BBoxValueSource that = (BBoxValueSource) o;
+
+    if (!strategy.equals(that.strategy)) return false;
+
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    return strategy.hashCode();
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/package-info.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/package-info.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/package-info.java
new file mode 100644
index 0000000..a229311
--- /dev/null
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+/**
+ * Bounding Box Spatial Strategy
+ * <p>
+ * Index a shape extent using 4 numeric fields and a flag to say if it crosses the dateline
+ */
+package org.apache.lucene.spatial.bbox;
\ No newline at end of file