Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/SpatialPrefixTree.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/SpatialPrefixTree.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/SpatialPrefixTree.java	(revision Shelved version)
@@ -37,11 +37,13 @@
   protected final int maxLevels;
 
   protected final SpatialContext ctx;
+  protected final boolean differentiateCoveredLeaves;//typically false unless you intend to use the distinction
 
-  public SpatialPrefixTree(SpatialContext ctx, int maxLevels) {
+  public SpatialPrefixTree(SpatialContext ctx, int maxLevels, boolean differentiateCoveredLeaves) {
     assert maxLevels > 0;
     this.ctx = ctx;
     this.maxLevels = maxLevels;
+    this.differentiateCoveredLeaves = differentiateCoveredLeaves;
   }
 
   public SpatialContext getSpatialContext() {
@@ -68,6 +70,11 @@
    * @return level [1 to maxLevels]
    */
   public abstract int getLevelForDistance(double dist);
+
+  /** Whether leaf bytes should be differentiated by cells that are "Covered" from those that aren't. */
+  public boolean isDifferentiateCoveredLeaves() {
+    return differentiateCoveredLeaves;
+  }
 
   /**
    * Given a cell having the specified level, returns the distance from opposite
Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/QuadPrefixTree.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/QuadPrefixTree.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/QuadPrefixTree.java	(revision Shelved version)
@@ -55,7 +55,9 @@
     @Override
     protected SpatialPrefixTree newSPT() {
       return new QuadPrefixTree(ctx,
-          maxLevels != null ? maxLevels : MAX_LEVELS_POSSIBLE);
+          ctx.getWorldBounds(),
+          maxLevels != null ? maxLevels : MAX_LEVELS_POSSIBLE,
+          differentiateCoveredLeaves == null ? false : differentiateCoveredLeaves);
     }
   }
 
@@ -77,9 +79,21 @@
   final int[]    levelS; // side
   final int[]    levelN; // number
 
+  public QuadPrefixTree(SpatialContext ctx) {
+    this(ctx, DEFAULT_MAX_LEVELS);
+  }
+
+  public QuadPrefixTree(SpatialContext ctx, int maxLevels) {
+    this(ctx, ctx.getWorldBounds(), maxLevels);
+  }
+
+  public QuadPrefixTree(SpatialContext ctx, Rectangle bounds, int maxLevels) {
+    this(ctx, bounds, maxLevels, false);
+  }
+
   public QuadPrefixTree(
-      SpatialContext ctx, Rectangle bounds, int maxLevels) {
-    super(ctx, maxLevels);
+      SpatialContext ctx, Rectangle bounds, int maxLevels, boolean differentiateCoveredLeaves) {
+    super(ctx, maxLevels, differentiateCoveredLeaves);
     this.xmin = bounds.getMinX();
     this.xmax = bounds.getMaxX();
     this.ymin = bounds.getMinY();
@@ -107,15 +121,6 @@
     }
   }
 
-  public QuadPrefixTree(SpatialContext ctx) {
-    this(ctx, DEFAULT_MAX_LEVELS);
-  }
-
-  public QuadPrefixTree(
-      SpatialContext ctx, int maxLevels) {
-    this(ctx, ctx.getWorldBounds(), maxLevels);
-  }
-
   @Override
   public Cell getWorldCell() {
     return new QuadCell(BytesRef.EMPTY_BYTES, 0, 0);
@@ -228,9 +233,6 @@
 
     @Override
     protected QuadPrefixTree getGrid() { return QuadPrefixTree.this; }
-
-    @Override
-    protected int getMaxLevels() { return maxLevels; }
 
     @Override
     protected Collection<Cell> getSubCells() {
Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/NumberRangePrefixTree.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/NumberRangePrefixTree.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/NumberRangePrefixTree.java	(revision Shelved version)
@@ -367,7 +367,9 @@
   protected final int maxTermLen; // how long could cell.getToken... (that is a leaf) possibly be?
 
   protected NumberRangePrefixTree(int[] maxSubCellsByLevel) {
-    super(DUMMY_CTX, maxSubCellsByLevel.length);
+    super(DUMMY_CTX,
+        maxSubCellsByLevel.length, //maxLevels
+        false);//differentiateCoveredLeaves==false; we have only 1 leaf type and 'covered' is implied
     this.maxSubCellsByLevel = maxSubCellsByLevel;
 
     // Fill termLenByLevel
@@ -597,6 +599,11 @@
     @Override
     public void setLeaf() {
       cellIsLeaf = true;
+    }
+
+    @Override
+    public boolean mightHaveAdjacentLeaf() {
+      return !isLeaf();//we only have one leaf type
     }
 
     @Override
Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/LegacyCell.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/LegacyCell.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/LegacyCell.java	(revision Shelved version)
@@ -33,7 +33,11 @@
   // Important: A LegacyCell doesn't share state for getNextLevelCells(), and
   //  LegacySpatialPrefixTree assumes this in its simplify tree logic.
 
-  private static final byte LEAF_BYTE = '+';//NOTE: must sort before letters & numbers
+  /** The leaf byte that is within (contained by) the shape it was generated from. */
+  private static final byte LEAF_COVERED_BYTE = '*';
+  /** The leaf byte for an approximated / edge that is partially overlapping the shape it was
+   * generated from. */
+  private static final byte LEAF_OVERLAP_BYTE = '+';//NOTE: must sort before letters & numbers
 
   //Arguably we could simply use a BytesRef, using an extra Object.
   private byte[] bytes;//generally bigger to potentially hold a leaf
@@ -64,17 +68,27 @@
     shape = null;
     this.bytes = bytes.bytes;
     this.b_off = bytes.offset;
-    this.b_len = (short) bytes.length;
+    this.b_len = bytes.length;
     readLeafAdjust();
   }
 
   private void readLeafAdjust() {
-    isLeaf = (b_len > 0 && bytes[b_off + b_len - 1] == LEAF_BYTE);
-    if (isLeaf)
+    isLeaf = false;
+    if (b_len == 0) {
+      return; // only for world cell (which isn't leaf)
+    }
+    final byte tailByte = bytes[b_off + b_len - 1];
+    if (tailByte == LEAF_COVERED_BYTE) {
+      isLeaf = true;
       b_len--;
-    if (getLevel() == getMaxLevels())
+      setShapeRel(SpatialRelation.WITHIN);
+    } else if (tailByte == LEAF_OVERLAP_BYTE) {
       isLeaf = true;
+      b_len--;
+    } else if (getLevel() == getMaxLevels()) {
+      isLeaf = true;
-  }
+    }
+  }
 
 //  @Override
 //  public void copyFrom(Cell source) {
@@ -88,9 +102,11 @@
 //    b_len = src.b_len;
 //  }
 
-  protected abstract SpatialPrefixTree getGrid();
+  protected abstract LegacyPrefixTree getGrid();
 
-  protected abstract int getMaxLevels();
+  protected final int getMaxLevels() {
+    return getGrid().getMaxLevels();
+  }
 
   @Override
   public SpatialRelation getShapeRel() {
@@ -112,11 +128,32 @@
     isLeaf = true;
   }
 
+  /** Returns whether this cell is covered by the shape from which it was generated from, as opposed to overlapping.
+   * It's always false if {@link SpatialPrefixTree#isDifferentiateCoveredLeaves()} is false.
+   * Precondition: isLeaf is true.
+   */
+  public boolean isLeafBytesCovered() {
+    assert isLeaf;
+    return getGrid().isDifferentiateCoveredLeaves() && getShapeRel() == SpatialRelation.WITHIN;
+  }
+
   @Override
+  public boolean mightHaveAdjacentLeaf() {
+    if (!isLeaf()) {
+      return true;//unless points-only?
+    }
+    assert LEAF_COVERED_BYTE < LEAF_OVERLAP_BYTE;
+    return shapeRel == SpatialRelation.WITHIN;
+  }
+
+  @Override
   public BytesRef getTokenBytesWithLeaf(BytesRef result) {
     result = getTokenBytesNoLeaf(result);
-    if (!isLeaf || getLevel() == getMaxLevels())
+    if (!isLeaf
+        || (getLevel() == getMaxLevels()) && !isLeafBytesCovered()) {
+      // At max levels, only use the no-leaf version if the leaf isn't covered or if it is but we don't differentiate
       return result;
+    }
     if (result.bytes.length < result.offset + result.length + 1) {
       assert false : "Not supposed to happen; performance bug";
       byte[] copy = new byte[result.length + 1];
@@ -124,7 +161,8 @@
       result.bytes = copy;
       result.offset = 0;
     }
-    result.bytes[result.offset + result.length++] = LEAF_BYTE;
+    result.bytes[result.offset + result.length++] =
+        (isLeafBytesCovered() ? LEAF_COVERED_BYTE : LEAF_OVERLAP_BYTE);
     return result;
   }
 
Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.java	(revision Shelved version)
@@ -52,10 +52,11 @@
 
   protected int prefixGridScanLevel;
 
-  //Formerly known as simplifyIndexedCells. Eventually will be removed. Only compatible with RPT
-  // and a LegacyPrefixTree.
+  //Formerly known as simplifyIndexedCells. Only compatible with LegacyPrefixTree at this time.
   protected boolean pruneLeafyBranches = true;
 
+  protected float pruneCoveredLeafRatio = 1f;//1f effectively disables.
+
   protected boolean multiOverlappingIndexedShapes = true;
 
   public RecursivePrefixTreeStrategy(SpatialPrefixTree grid, String fieldName) {
@@ -92,14 +93,29 @@
     return pruneLeafyBranches;
   }
 
-  /** An optional hint affecting non-point shapes: it will
-   * simplify/aggregate sets of complete leaves in a cell to its parent, resulting in ~20-25%
-   * fewer indexed cells. However, it will likely be removed in the future. (default=true)
+  /** Toggles leafy branch pruning; defaults to true if unset. This only affects indexed non-point shapes. It will
+   * simplify/aggregate sets of complete leaves in a cell to its parent, resulting in ~20-50% fewer indexed cells.
+   * The most pruning is for a quad tree as there are more opportunities to prune.
    */
   public void setPruneLeafyBranches(boolean pruneLeafyBranches) {
     this.pruneLeafyBranches = pruneLeafyBranches;
   }
 
+  public float getPruneCoveredLeafRatio() {
+    return pruneCoveredLeafRatio;
+  }
+
+  /** If more than this ratio of covered leaves are found at detailLevel - 1 for an indexed shape cell, then we don't
+   * prune it. At courser levels (smaller level), this is effectively always 0, disabling the prune if any covered cells
+   * are found. This is only pertinent if
+   * {@link org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree#isDifferentiateCoveredLeaves()} is enabled. It
+   * defaults to 1.0 which effectively un-constrains pruning at the max depth, with the trade-off that search
+   * algorithms leveraging this leaf distinction won't see a distinction on these pruned edges.
+   */
+  public void setPruneCoveredLeafRatio(float pruneCoveredLeafRatio) {
+    this.pruneCoveredLeafRatio = pruneCoveredLeafRatio;
+  }
+
   @Override
   public String toString() {
     StringBuilder str = new StringBuilder(getClass().getSimpleName()).append('(');
@@ -128,7 +144,7 @@
   /** Returns true if cell was added as a leaf. If it wasn't it recursively descends. */
   private boolean recursiveTraverseAndPrune(Cell cell, Shape shape, int detailLevel, List<Cell> result) {
     // Important: this logic assumes Cells don't share anything with other cells when
-    // calling cell.getNextLevelCells(). This is only true for LegacyCell.
+    // calling cell.getNextLevelCells(). This is only true for LegacyCell. TODO make universal
     if (!(cell instanceof LegacyCell))
       throw new IllegalStateException("pruneLeafyBranches must be disabled for use with grid "+grid);
 
@@ -142,23 +158,30 @@
     if (cell.getLevel() != 0)
       result.add(cell);
 
+    //Recurse, and track how many are leaves & covered leaves
     int leaves = 0;
+    int coveredLeaves = 0;
     CellIterator subCells = cell.getNextLevelCells(shape);
     while (subCells.hasNext()) {
       Cell subCell = subCells.next();
-      if (recursiveTraverseAndPrune(subCell, shape, detailLevel, result))
+      if (recursiveTraverseAndPrune(subCell, shape, detailLevel, result)) {
         leaves++;
+        if (((LegacyCell)result.get(result.size() - 1)).isLeafBytesCovered()) {
+          coveredLeaves++;
-    }
+        }
-    //can we prune?
-    if (leaves == ((LegacyCell)cell).getSubCellsSize() && cell.getLevel() != 0) {
-      //Optimization: substitute the parent as a leaf instead of adding all
-      // children as leaves
-
+      }
+    }
+    //Can we prune?
+    final int subCellsSize = ((LegacyCell) cell).getSubCellsSize();
+    final int coveredThreshold =
+        (cell.getLevel() == detailLevel - 1) ? (int) (subCellsSize * pruneCoveredLeafRatio) : 0;
+    if (cell.getLevel() != 0 && leaves == subCellsSize
+        && coveredLeaves <= coveredThreshold) {
       //remove the leaves
       do {
         result.remove(result.size() - 1);//remove last
       } while (--leaves > 0);
-      //add cell as the leaf
+      //set parent to be a leaf
       cell.setLeaf();
       return true;
     }
Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/WithinPrefixTreeFilter.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/WithinPrefixTreeFilter.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/WithinPrefixTreeFilter.java	(revision Shelved version)
@@ -171,8 +171,8 @@
       }
 
       @Override
-      protected boolean visitPrefix(Cell cell) throws IOException {
-        //cell.relate is based on the bufferedQueryShape; we need to examine what
+      protected boolean visitPrefix(Cell cell, SpatialRelation bufferQueryRel) throws IOException {
+        //bufferQueryRel is based on the bufferedQueryShape; we need to examine what
         // the relation is against the queryShape
         SpatialRelation visitRelation = cell.getShape().relate(queryShape);
         if (cell.getLevel() == detailLevel) {
@@ -189,7 +189,7 @@
       }
 
       @Override
-      protected void visitLeaf(Cell cell) throws IOException {
+      protected void visitLeaf(Cell cell, SpatialRelation bufferQueryRel) throws IOException {
         if (allCellsIntersectQuery(cell))
           collectDocs(inside);
         else
@@ -220,7 +220,7 @@
 
       @Override
       protected void visitScanned(Cell cell) throws IOException {
-        visitLeaf(cell);//collects as we want, even if not a leaf
+        visitLeaf(cell, null);//collects as we want, even if not a leaf
 //        if (cell.isLeaf()) {
 //          visitLeaf(cell);
 //        } else {
Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/GeohashPrefixTree.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/GeohashPrefixTree.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/GeohashPrefixTree.java	(revision Shelved version)
@@ -51,12 +51,17 @@
     @Override
     protected SpatialPrefixTree newSPT() {
       return new GeohashPrefixTree(ctx,
-          maxLevels != null ? maxLevels : GeohashPrefixTree.getMaxLevelsPossible());
+          maxLevels != null ? maxLevels : GeohashPrefixTree.getMaxLevelsPossible(),
+          differentiateCoveredLeaves == null ? false : differentiateCoveredLeaves);
     }
   }
 
   public GeohashPrefixTree(SpatialContext ctx, int maxLevels) {
-    super(ctx, maxLevels);
+    this(ctx, maxLevels, false);
+  }
+
+  public GeohashPrefixTree(SpatialContext ctx, int maxLevels, boolean differentiateCoveredLeaves) {
+    super(ctx, maxLevels, differentiateCoveredLeaves);
     Rectangle bounds = ctx.getWorldBounds();
     if (bounds.getMinX() != -180)
       throw new IllegalArgumentException("Geohash only supports lat-lon world bounds. Got "+bounds);
@@ -104,8 +109,7 @@
     GhCell(String geohash) {
       super(stringToBytesPlus1(geohash), 0, geohash.length());
       this.geohash = geohash;
-      if (isLeaf() && getLevel() < getMaxLevels())//we don't have a leaf byte at max levels (an opt)
-        this.geohash = geohash.substring(0, geohash.length() - 1);
+      assert ! isLeaf() || getLevel() == getMaxLevels() : "this constructor isn't for leaves";
     }
 
     GhCell(byte[] bytes, int off, int len) {
@@ -114,9 +118,6 @@
 
     @Override
     protected GeohashPrefixTree getGrid() { return GeohashPrefixTree.this; }
-
-    @Override
-    protected int getMaxLevels() { return maxLevels; }
 
     @Override
     protected void readCell(BytesRef bytesRef) {
Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/IntersectsPrefixTreeFilter.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/IntersectsPrefixTreeFilter.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/IntersectsPrefixTreeFilter.java	(revision Shelved version)
@@ -45,7 +45,7 @@
 
   @Override
   public DocIdSet getDocIdSet(LeafReaderContext context, Bits acceptDocs) throws IOException {
-    /* Possible optimizations (in IN ADDITION TO THOSE LISTED IN VISITORTEMPLATE):
+    /* Possible optimizations (IN ADDITION TO THOSE LISTED IN VISITORTEMPLATE):
 
     * If docFreq is 1 (or < than some small threshold), then check to see if we've already
       collected it; if so short-circuit. Don't do this just for point data, as there is
@@ -68,8 +68,8 @@
       }
 
       @Override
-      protected boolean visitPrefix(Cell cell) throws IOException {
-        if (cell.getShapeRel() == SpatialRelation.WITHIN || cell.getLevel() == detailLevel) {
+      protected boolean visitPrefix(Cell cell, SpatialRelation queryRel) throws IOException {
+        if (queryRel == SpatialRelation.WITHIN || cell.getLevel() == detailLevel) {
           collectDocs(results);
           return false;
         }
@@ -77,7 +77,7 @@
       }
 
       @Override
-      protected void visitLeaf(Cell cell) throws IOException {
+      protected void visitLeaf(Cell cell, SpatialRelation queryRel) throws IOException {
         collectDocs(results);
       }
 
Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/AbstractPrefixTreeFilter.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/AbstractPrefixTreeFilter.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/AbstractPrefixTreeFilter.java	(revision Shelved version)
@@ -26,9 +26,11 @@
 import org.apache.lucene.index.Terms;
 import org.apache.lucene.index.TermsEnum;
 import org.apache.lucene.search.Filter;
+import org.apache.lucene.spatial.prefix.tree.Cell;
 import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
 import org.apache.lucene.util.BitSet;
 import org.apache.lucene.util.Bits;
+import org.apache.lucene.util.BytesRef;
 
 /**
  * Base class for Lucene Filters on SpatialPrefixTree fields.
@@ -82,6 +84,10 @@
     protected TermsEnum termsEnum;//remember to check for null!
     protected PostingsEnum postingsEnum;
 
+    protected BytesRef thisTerm;//the result of termsEnum.term()
+    protected Cell indexedCell;//Cell wrapper of thisTerm. Always updated when thisTerm is.
+    BytesRef seekTerm = new BytesRef();//temp; see seek()
+
     public BaseTermsEnumTraverser(LeafReaderContext context, Bits acceptDocs) throws IOException {
       this.context = context;
       LeafReader reader = context.reader();
@@ -98,6 +104,37 @@
       bitSet.or(postingsEnum);
     }
 
+    /** Advanced the terms to the next term and updated {@link #indexedCell}. Returns false if at the end. */
+    protected boolean nextTerm() throws IOException {
+      if ((thisTerm = termsEnum.next()) == null)
+        return false;
+      indexedCell = grid.readCell(thisTerm, indexedCell);
+      return true;
+    }
+
+    /** Seeks terms to the specified cell, ignoring leaf flag. Updates {@link #indexedCell}. */
+    protected TermsEnum.SeekStatus seek(Cell cell) throws IOException {
+      seekTerm = cell.getTokenBytesNoLeaf(seekTerm);
+      final TermsEnum.SeekStatus seekStatus = termsEnum.seekCeil(seekTerm);
+      if (seekStatus == TermsEnum.SeekStatus.END) {
+        thisTerm = null;//all done
+        indexedCell = null;
+      } else {
+        thisTerm = termsEnum.term();
+        indexedCell = grid.readCell(thisTerm, indexedCell);
+      }
+      return seekStatus;
+    }
+
+    /** Returns true if indexedCell is a leaf with the same prefix bytes as curVNode.cell. */
+    protected boolean isIndexedLeafOfQueryCellPrefix(Cell queryCell, boolean assumePrefix) {
+      //TODO should cell have equalsNoLeaf()
+      final boolean result = indexedCell.isLeaf()
+          && indexedCell.getLevel() == queryCell.getLevel()
+          && (assumePrefix || indexedCell.compareToNoLeaf(queryCell) == 0);
+      assert !result || !assumePrefix || indexedCell.compareToNoLeaf(queryCell) == 0 : "assumePrefix should be false";
+      return result;
+    }
   }
 
 }
Index: lucene/spatial/src/test/org/apache/lucene/spatial/prefix/RandomSpatialOpFuzzyPrefixTreeTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/test/org/apache/lucene/spatial/prefix/RandomSpatialOpFuzzyPrefixTreeTest.java	(revision Local version)
+++ lucene/spatial/src/test/org/apache/lucene/spatial/prefix/RandomSpatialOpFuzzyPrefixTreeTest.java	(revision Shelved version)
@@ -107,7 +107,7 @@
     //A fairly shallow grid, and default 2.5% distErrPct
     if (maxLevels == -1)
       maxLevels = randomIntBetween(1, 8);//max 64k cells (4^8), also 256*256
-    this.grid = new QuadPrefixTree(ctx, maxLevels);
+    this.grid = new QuadPrefixTree(ctx, ctx.getWorldBounds(), maxLevels, randomBoolean());
     this.strategy = newRPT();
   }
 
@@ -116,7 +116,7 @@
     //A fairly shallow grid, and default 2.5% distErrPct
     if (maxLevels == -1)
       maxLevels = randomIntBetween(1, 3);//max 16k cells (32^3)
-    this.grid = new GeohashPrefixTree(ctx, maxLevels);
+    this.grid = new GeohashPrefixTree(ctx, maxLevels, randomBoolean());
     this.strategy = newRPT();
   }
 
@@ -422,7 +422,10 @@
         continue;
       cellShapes.add(cell.getShape());
     }
-    return new ShapeCollection<>(cellShapes, ctx).getBoundingBox();
+    // Due to Spatial4j bug in geo longitude world-wrap, we use ctx2D knowing that our test data doesn't cross the
+    // dateline.
+    assert !snapMe.getBoundingBox().getCrossesDateLine();
+    return new ShapeCollection<>(cellShapes, ctx2D).getBoundingBox();
   }
 
   /**
Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/LegacyPrefixTree.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/LegacyPrefixTree.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/LegacyPrefixTree.java	(revision Shelved version)
@@ -27,9 +27,10 @@
 
 /** The base for the original two SPT's: Geohash and Quad. Don't subclass this for new SPTs.
  * @lucene.internal */
-abstract class LegacyPrefixTree extends SpatialPrefixTree {
-  public LegacyPrefixTree(SpatialContext ctx, int maxLevels) {
-    super(ctx, maxLevels);
+public abstract class LegacyPrefixTree extends SpatialPrefixTree {
+
+  public LegacyPrefixTree(SpatialContext ctx, int maxLevels, boolean differentiateCoveredLeaves) {
+    super(ctx, maxLevels, differentiateCoveredLeaves);
   }
 
   public double getDistanceForLevel(int level) {
Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/Cell.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/Cell.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/Cell.java	(revision Shelved version)
@@ -40,7 +40,9 @@
 //  void copyFrom(Cell source);
 
   /** Gets the relationship this cell has with the shape from which it was filtered from, assuming it came from a
-   * {@link CellIterator}. Arguably it belongs there but it's very convenient here. */
+   * {@link CellIterator}. Arguably it belongs there but it's very convenient here. If The Cell came from an indexed
+   * term then the result will be {@link com.spatial4j.core.shape.SpatialRelation#WITHIN} if the leaf was covered by
+   * the shape from which it was generated. */
   SpatialRelation getShapeRel();
 
   /** See {@link #getShapeRel()}.
@@ -59,6 +61,9 @@
    * Note: not supported at level 0.
    * @lucene.internal */
   void setLeaf();
+
+  /** nocommit */
+  boolean mightHaveAdjacentLeaf();
 
   /**
    * Returns the bytes for this cell, with a leaf byte <em>if this is a leaf cell</em>.
Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/ContainsPrefixTreeFilter.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/ContainsPrefixTreeFilter.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/ContainsPrefixTreeFilter.java	(revision Shelved version)
@@ -93,10 +93,6 @@
       }
     }
 
-    BytesRef seekTerm = new BytesRef();//temp; see seek()
-    BytesRef thisTerm;//current term in termsEnum
-    Cell indexedCell;//the cell wrapper around thisTerm
-
     /** This is the primary algorithm; recursive.  Returns null if finds none. */
     private SmallDocSet visit(Cell cell, Bits acceptContains) throws IOException {
 
@@ -114,16 +110,17 @@
       CellIterator subCells = cell.getNextLevelCells(subCellsFilter);
       while (subCells.hasNext()) {
         Cell subCell = subCells.next();
-        if (!seek(subCell)) {
+        if (!leapFrog(subCell)) {
           combinedSubResults = null;
         } else if (subCell.getLevel() == detailLevel) {
-          combinedSubResults = getDocs(subCell, acceptContains);
+          combinedSubResults = getPrefixAndLeafDocs(subCell, acceptContains);
         } else if (!multiOverlappingIndexedShapes &&
             subCell.getShapeRel() == SpatialRelation.WITHIN) {
           combinedSubResults = getLeafDocs(subCell, acceptContains);
         } else {
           //OR the leaf docs with all child results
           SmallDocSet leafDocs = getLeafDocs(subCell, acceptContains);
+          // TODO opt: if we don't have a prefix then there will be no sub-cells to visit
           SmallDocSet subDocs = visit(subCell, acceptContains); //recursion
           combinedSubResults = union(leafDocs, subDocs);
         }
@@ -136,7 +133,8 @@
       return combinedSubResults;
     }
 
-    private boolean seek(Cell cell) throws IOException {
+    /** Return true if we're at the desired prefix; seek forward if we need to. */
+    private boolean leapFrog(Cell cell) throws IOException {
       if (thisTerm == null)
         return false;
       final int compare = indexedCell.compareToNoLeaf(cell);
@@ -146,14 +144,10 @@
         return true; // already there!
       } else {//compare > 0
         //seek!
-        seekTerm = cell.getTokenBytesNoLeaf(seekTerm);
-        final TermsEnum.SeekStatus seekStatus = termsEnum.seekCeil(seekTerm);
+        final TermsEnum.SeekStatus seekStatus = seek(cell);
         if (seekStatus == TermsEnum.SeekStatus.END) {
-          thisTerm = null;//all done
           return false;
         }
-        thisTerm = termsEnum.term();
-        indexedCell = grid.readCell(thisTerm, indexedCell);
         if (seekStatus == TermsEnum.SeekStatus.FOUND) {
           return true;
         }
@@ -161,51 +155,53 @@
       }
     }
 
-    /** Get prefix & leaf docs at this cell. */
-    private SmallDocSet getDocs(Cell cell, Bits acceptContains) throws IOException {
+    /** Get prefix & leaf docs at this cell. Called when we've reached detailLevel. */
+    private SmallDocSet getPrefixAndLeafDocs(Cell cell, Bits acceptContains) throws IOException {
       assert indexedCell.compareToNoLeaf(cell) == 0;
-      //called when we've reached detailLevel.
-      if (indexedCell.isLeaf()) {//only a leaf
-        SmallDocSet result = collectDocs(acceptContains);
-        nextTerm();
-        return result;
-      } else {
-        SmallDocSet docsAtPrefix = collectDocs(acceptContains);
+
+      final boolean hadPrefix = !indexedCell.isLeaf();
+      //collect prefix
+      SmallDocSet result = null;
+      if (hadPrefix) {
+        result = collectDocs(result, acceptContains);
         if (!nextTerm()) {
-          return docsAtPrefix;
+          return result;
         }
-        //collect leaf too
-        if (indexedCell.isLeaf() && indexedCell.compareToNoLeaf(cell) == 0) {
-          SmallDocSet docsAtLeaf = collectDocs(acceptContains);
-          nextTerm();
-          return union(docsAtPrefix, docsAtLeaf);
-        } else {
-          return docsAtPrefix;
-        }
+      }
+
+      return collectLeaf(cell, acceptContains, hadPrefix, result);
-      }
+    }
-    }
 
     /** Gets docs on the leaf of the given cell, _if_ there is a leaf cell, otherwise null. */
     private SmallDocSet getLeafDocs(Cell cell, Bits acceptContains) throws IOException {
       assert indexedCell.compareToNoLeaf(cell) == 0;
+
+      final boolean hadPrefix = !indexedCell.isLeaf();
       //Advance past prefix if we're at a prefix; return null if no leaf
-      if (!indexedCell.isLeaf()) {
-        if (!nextTerm() || !indexedCell.isLeaf() || indexedCell.getLevel() != cell.getLevel()) {
+      if (hadPrefix) {
+        if (!nextTerm()) {
           return null;
         }
       }
-      SmallDocSet result = collectDocs(acceptContains);
+
+      return collectLeaf(cell, acceptContains, hadPrefix, null);
+    }
+
+    private SmallDocSet collectLeaf(Cell cell, Bits acceptContains, boolean assumePrefix, SmallDocSet result)
+        throws IOException {
+      if (isIndexedLeafOfQueryCellPrefix(cell, assumePrefix)) {
+        result = collectDocs(null, acceptContains);
+        if (nextTerm()) {
+          //collect secondary leaf
+          if (isIndexedLeafOfQueryCellPrefix(cell, assumePrefix)) {
+            result = collectDocs(result, acceptContains);
-      nextTerm();
+            nextTerm();
+          }
+        }
+      }
       return result;
     }
 
-    private boolean nextTerm() throws IOException {
-      if ((thisTerm = termsEnum.next()) == null)
-        return false;
-      indexedCell = grid.readCell(thisTerm, indexedCell);
-      return true;
-    }
-
     private SmallDocSet union(SmallDocSet aSet, SmallDocSet bSet) {
       if (bSet != null) {
         if (aSet == null)
@@ -215,21 +211,19 @@
       return aSet;
     }
 
-    private SmallDocSet collectDocs(Bits acceptContains) throws IOException {
-      SmallDocSet set = null;
-
+    private SmallDocSet collectDocs(SmallDocSet result, Bits acceptContains) throws IOException {
       postingsEnum = termsEnum.postings(acceptContains, postingsEnum, PostingsEnum.NONE);
       int docid;
       while ((docid = postingsEnum.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
-        if (set == null) {
+        if (result == null) {
           int size = termsEnum.docFreq();
           if (size <= 0)
             size = 16;
-          set = new SmallDocSet(size);
+          result = new SmallDocSet(size);
         }
-        set.set(docid);
+        result.set(docid);
       }
-      return set;
+      return result;
     }
 
   }//class ContainsVisitor
Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/AbstractVisitingPrefixTreeFilter.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/AbstractVisitingPrefixTreeFilter.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/AbstractVisitingPrefixTreeFilter.java	(revision Shelved version)
@@ -29,7 +29,6 @@
 import org.apache.lucene.spatial.prefix.tree.CellIterator;
 import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
 import org.apache.lucene.util.Bits;
-import org.apache.lucene.util.BytesRef;
 
 /**
  * Traverses a {@link SpatialPrefixTree} indexed field, using the template and
@@ -89,7 +88,7 @@
    * which by default finds the top cells that intersect {@code queryShape}. If
    * there isn't an indexed cell for a corresponding cell returned for this
    * method then it's short-circuited until it finds one, at which point
-   * {@link #visitPrefix(org.apache.lucene.spatial.prefix.tree.Cell)} is called. At
+   * {@link #visitPrefix(org.apache.lucene.spatial.prefix.tree.Cell, com.spatial4j.core.shape.SpatialRelation)} is called. At
    * some depths, of the tree, the algorithm switches to a scanning mode that
    * calls {@link #visitScanned(org.apache.lucene.spatial.prefix.tree.Cell)}
    * for each leaf cell found.
@@ -120,11 +119,7 @@
     //
 
     private VNode curVNode;//current pointer, derived from query shape
-    private BytesRef curVNodeTerm = new BytesRef();//curVNode.cell's term, without leaf. in main loop only
 
-    private BytesRef thisTerm;//the result of termsEnum.term()
-    private Cell indexedCell;//Cell wrapper of thisTerm. Always updated when thisTerm is.
-
     public VisitorTemplate(LeafReaderContext context, Bits acceptDocs) throws IOException {
       super(context, acceptDocs);
     }
@@ -180,36 +175,52 @@
         if (compare < 0) {
           // The indexed cell is before; seek ahead to query cell:
           //      Seek !
-          curVNode.cell.getTokenBytesNoLeaf(curVNodeTerm);
-          TermsEnum.SeekStatus seekStatus = termsEnum.seekCeil(curVNodeTerm);
+          TermsEnum.SeekStatus seekStatus = seek(curVNode.cell);
           if (seekStatus == TermsEnum.SeekStatus.END)
             break; // all done
-          thisTerm = termsEnum.term();
-          indexedCell = grid.readCell(thisTerm, indexedCell);
           if (seekStatus == TermsEnum.SeekStatus.NOT_FOUND) {
             // Did we find a leaf of the cell we were looking for or something after?
-            if (!indexedCell.isLeaf() || indexedCell.compareToNoLeaf(curVNode.cell) != 0)
+            if (!isIndexedLeafOfQueryCellPrefix(curVNode.cell, false))
               continue; // The indexed cell is after; continue loop to next query cell
           }
         }
         // indexedCell == queryCell (disregarding leaf).
 
+        //TODO should rel to query shape be passed as a param to visit methods?  Would disambiguate
+        // what cell.getShapeRel is -- it could always be the indexed one.
+
         // If indexedCell is a leaf then there's no prefix (prefix sorts before) -- just visit and continue
         if (indexedCell.isLeaf()) {
-          visitLeaf(indexedCell);//TODO or query cell? Though shouldn't matter.
+          visitLeaf(indexedCell, curVNode.cell.getShapeRel());
+          boolean checkAdjLeaf = indexedCell.mightHaveAdjacentLeaf();
           if (!nextTerm()) break;
+
+          //another leaf (leaf type differentiation)
+          if (checkAdjLeaf && isIndexedLeafOfQueryCellPrefix(curVNode.cell, false)) {
+            visitLeaf(indexedCell, curVNode.cell.getShapeRel());
+            if (!nextTerm()) break;
+          }
           continue;
         }
-        // If a prefix (non-leaf) then visit; see if we descend.
-        final boolean descend = visitPrefix(curVNode.cell);//need to use curVNode.cell not indexedCell
+
+        // If a prefix (non-leaf) then visit; returns true if we should descend.
+        final boolean descend = visitPrefix(indexedCell, curVNode.cell.getShapeRel());
+        boolean checkAdjLeaf = indexedCell.mightHaveAdjacentLeaf();
         if (!nextTerm()) break;
+
         // Check for adjacent leaf with the same prefix
-        if (indexedCell.isLeaf() && indexedCell.getLevel() == curVNode.cell.getLevel()) {
-          visitLeaf(indexedCell);//TODO or query cell? Though shouldn't matter.
+        if (checkAdjLeaf && isIndexedLeafOfQueryCellPrefix(curVNode.cell, true)) {//true: can assume same prefix
+          visitLeaf(indexedCell, curVNode.cell.getShapeRel());
+          checkAdjLeaf = indexedCell.mightHaveAdjacentLeaf();
           if (!nextTerm()) break;
+
+          //another leaf (leaf type differentiation)
+          if (checkAdjLeaf && isIndexedLeafOfQueryCellPrefix(curVNode.cell, true)) {//true: can assume same prefix
+            visitLeaf(indexedCell, curVNode.cell.getShapeRel());
+            if (!nextTerm()) break;
-        }
+          }
+        }
 
-
         if (descend) {
           addIntersectingChildren();
         }
@@ -219,7 +230,7 @@
       return finish();
     }
 
-    /** Called initially, and whenever {@link #visitPrefix(org.apache.lucene.spatial.prefix.tree.Cell)}
+    /** Called initially, and whenever {@link #visitPrefix(org.apache.lucene.spatial.prefix.tree.Cell, com.spatial4j.core.shape.SpatialRelation)}
      * returns true. */
     private void addIntersectingChildren() throws IOException {
       assert thisTerm != null;
@@ -277,13 +288,6 @@
       }
     }
 
-    private boolean nextTerm() throws IOException {
-      if ((thisTerm = termsEnum.next()) == null)
-        return false;
-      indexedCell = grid.readCell(thisTerm, indexedCell);
-      return true;
-    }
-
     /** Used for {@link VNode#children}. */
     private class VNodeCellIterator implements Iterator<VNode> {
 
@@ -320,38 +324,43 @@
 
     /**
      * Visit an indexed non-leaf cell. The presence of a prefix cell implies
-     * there are leaf cells at further levels. The cell passed should have it's
-     * {@link org.apache.lucene.spatial.prefix.tree.Cell#getShapeRel()} set
-     * relative to the filtered shape.
+     * there are leaf cells at further levels.
      *
      * @param cell An intersecting cell; not a leaf.
+     * @param queryRel The relationship to the filtered/query shape.
      * @return true to descend to more levels.
      */
-    protected abstract boolean visitPrefix(Cell cell) throws IOException;
+    protected abstract boolean visitPrefix(Cell cell, SpatialRelation queryRel) throws IOException;
 
     /**
      * Called when an indexed leaf cell is found. An
      * indexed leaf cell usually means associated documents won't be found at
      * further detail levels.  However, if a document has
      * multiple overlapping shapes at different resolutions, then this isn't true.
+     * {@link org.apache.lucene.spatial.prefix.tree.Cell#getShapeRel()} will
+     * be {@link com.spatial4j.core.shape.SpatialRelation#WITHIN} if this leaf cell
+     * was covered by the indexed shape from which it was generated. It's not the
+     * query shape relation.
+     *
+     * @param cell A leaf cell found in the index (from TermsEnum)
+     * @param queryRel The relationship to the filtered/query shape.
      */
-    protected abstract void visitLeaf(Cell cell) throws IOException;
+    protected abstract void visitLeaf(Cell cell, SpatialRelation queryRel) throws IOException;
 
     /**
      * The cell is either indexed as a leaf or is the last level of detail. It
      * might not even intersect the query shape, so be sure to check for that.
      * The default implementation will check that and if passes then call
-     * {@link #visitLeaf(org.apache.lucene.spatial.prefix.tree.Cell)} or
-     * {@link #visitPrefix(org.apache.lucene.spatial.prefix.tree.Cell)}.
+     * {@link #visitLeaf(org.apache.lucene.spatial.prefix.tree.Cell, com.spatial4j.core.shape.SpatialRelation)} or
+     * {@link #visitPrefix(org.apache.lucene.spatial.prefix.tree.Cell, com.spatial4j.core.shape.SpatialRelation)}.
      */
     protected void visitScanned(Cell cell) throws IOException {
       final SpatialRelation relate = cell.getShape().relate(queryShape);
       if (relate.intersects()) {
-        cell.setShapeRel(relate);//just being pedantic
         if (cell.isLeaf()) {
-          visitLeaf(cell);
+          visitLeaf(cell, relate);
         } else {
-          visitPrefix(cell);
+          visitPrefix(cell, relate);
         }
       }
     }
Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/SpatialPrefixTreeFactory.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/SpatialPrefixTreeFactory.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/SpatialPrefixTreeFactory.java	(revision Shelved version)
@@ -34,10 +34,12 @@
   public static final String PREFIX_TREE = "prefixTree";
   public static final String MAX_LEVELS = "maxLevels";
   public static final String MAX_DIST_ERR = "maxDistErr";
+  public static final String DIFF_COVERED_LEAVES = "differentiateCoveredLeaves";
 
   protected Map<String, String> args;
   protected SpatialContext ctx;
   protected Integer maxLevels;
+  protected Boolean differentiateCoveredLeaves;
 
   /**
    * The factory  is looked up via "prefixTree" in args, expecting "geohash" or "quad".
@@ -68,6 +70,14 @@
     this.args = args;
     this.ctx = ctx;
     initMaxLevels();
+    initDiffCoveredLeaves();
+  }
+
+  private void initDiffCoveredLeaves() {
+    String diffCovLeaves = args.get(DIFF_COVERED_LEAVES);
+    if (diffCovLeaves != null) {
+      differentiateCoveredLeaves = Boolean.parseBoolean(diffCovLeaves);
+    }
   }
 
   protected void initMaxLevels() {
@@ -95,4 +105,4 @@
 
   protected abstract SpatialPrefixTree newSPT();
 
-}
+}
\ No newline at end of file
Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/PrefixTreeFacetCounter.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/PrefixTreeFacetCounter.java	(revision Local version)
+++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/PrefixTreeFacetCounter.java	(revision Shelved version)
@@ -20,6 +20,7 @@
 import java.io.IOException;
 
 import com.spatial4j.core.shape.Shape;
+import com.spatial4j.core.shape.SpatialRelation;
 import org.apache.lucene.index.IndexReaderContext;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.PostingsEnum;
@@ -149,11 +150,11 @@
           }
 
           @Override
-          protected boolean visitPrefix(Cell cell) throws IOException {
+          protected boolean visitPrefix(Cell cell, SpatialRelation queryRel) throws IOException {
             // At facetLevel...
             if (cell.getLevel() == facetLevel) {
               // Count docs
-              visitLeaf(cell);//we're not a leaf but we treat it as such at facet level
+              visitLeaf(cell, queryRel);//we're not a leaf but we treat it as such at facet level
               return false;//don't descend further; this is enough detail
             }
 
@@ -171,7 +172,7 @@
           }
 
           @Override
-          protected void visitLeaf(Cell cell) throws IOException {
+          protected void visitLeaf(Cell cell, SpatialRelation queryRel) throws IOException {
             final int count = countDocsAtThisTerm();
             if (count > 0) {
               facetVisitor.visit(cell, count);
