All findings · Lucene finding · LFV-369 · bbox-overlap-ratio-two-piece-dateline-intersection

BBoxStrategy's overlap-ratio score counts only one of the two overlaps of two dateline-crossing boxes, so the score is too low

When a query box and a document box both cross the dateline and overlap twice, BBoxStrategy's overlap-ratio score counts only the overlap across the dateline: 0.1032 instead of 0.1548 in the example, so the document ranks too low. Confirmed on main and 15 releases since 4.10.0; spatial matching is unaffected.

Summary

Priority
Low, rank 437A spatial-extras overlap-ratio score comes out too low, which affects ranking only and only for pairs of very wide boxes that both cross the dateline.
Impact
silent wrong results
Since
4.10.0 (LUCENE-5714/5779, 2014)introduced 2014-07-08 · over 12 years
Affected
4.10.0 to 10.5.2 (15 releases run, the line unchanged in all 103 release tags read), 10.6.0 (by reading) and main
Status
documented
Found by
bounded-exhaustive checking of the real jar against the class-javadoc formula

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

BBoxStrategy (spatial-extras) indexes rectangles and can rank documents by how much their box overlaps the query box (makeOverlapRatioValueSource). A box with minX > maxX crosses the dateline and covers everything except one gap. Two such boxes whose gaps are strictly separated (and whose latitudes overlap) meet twice: across the dateline and on the far side.

Two longitude bands: the query box covers 10°E east across the dateline to 170°W (180°), the document box covers 170°E east across the dateline to 20°E (210°). Their overlap has two pieces, 170°E to 170°W across the dateline (20°) and 10°E to 20°E (10°), 30° in total; Lucene uses a width of 20°, so the score is 0.1032 instead of 0.1548.
The longitude circle with the query box (10°E east to 170°W) and the document box (170°E east to 20°E): they overlap across the dateline (20°) and again from 10°E to 20°E (10°), but the score counts only the first piece. Select the still to open it at full size. Generated with AI, checked against the code.
  1. In the default geo context, index a document box that runs from 170°E east across the dateline to 20°E (rect(170, 20, -10, 10), 210° wide) with BBoxStrategy, and score it with makeOverlapRatioValueSource(rect(10, -170, -10, 10), 0.5): a query box from 10°E across the dateline to 170°W (180° wide).
  2. The two boxes overlap twice: from 170°E to 170°W across the dateline (20°) and from 10°E to 20°E on the far side of the globe (10°). You expect the documented score with an intersection of 30° x 20°: 600/3600 x 0.5 + 600/4200 x 0.5 = 0.1548.
  3. You get 0.1032. When both boxes cross, the code takes the width as min(maxX) - max(minX) + 360 = -170 - 170 + 360 = 20°: only the piece across the dateline.
  4. Move both boxes 30° west, so only the document crosses, and the score is 0.1548: that branch adds both pieces. Sorting by the score puts a worse match, a box overlapping only 10°E to 35°E (0.1389, computed correctly), above this document.
  5. No error: BBoxStrategy's spatial matching is unaffected, this defect throws nothing, and it only ever lowers the score.

Who is affected

Applications that rank, sort or boost by BBoxStrategy's overlap ratio in a geo context (an opt-in, experimental spatial-extras API; Solr exposes it as score=overlapRatio), when the query box and a document box both cross the dateline and overlap twice. One document suffices, with default settings. Spatial matching is unaffected; a filter built on this score is affected.

Mechanism, terms and numbers693 words

BBoxStrategy.makeOverlapRatioValueSource scores a document by how much its box overlaps the query box, with the documented formula intersectionArea / queryArea * p + intersectionArea / targetArea * (1 - p). A box that crosses the dateline covers the whole longitude circle except one gap. When the query box and the document box both cross the dateline, their latitudes overlap and their gaps are strictly separated (disjoint and not touching), each box covers the other's gap and the two boxes overlap twice: once across the dateline and once on the far side of the globe (this needs their widths to add up to more than 360°, but that alone is not enough). The both-cross branch of BBoxOverlapRatioValueSource.score computes the width as min(maxX) - max(minX) + 360, which is only the piece across the dateline. Query rect(10, -170, -10, 10) and document rect(170, 20, -10, 10) share 170..-170 (20°) and 10..20 (10°), but the score uses a width of 20 instead of 30: 0.10317460317460317 instead of 0.15476190476190477 at p = 0.5. The same pair moved 30° west, so only the document crosses, scores 0.15476190476190477. Sorting by the value puts a worse match first. BBoxStrategy's spatial predicates and the index are unaffected; a filter an application builds on this score is affected. This defect only ever lowers the score and does not throw. Confirmed on main and on every release run (15, from 4.10.0, the class's first release, to 10.5.2); every release tag read has the line. Fix: add the second piece in the both-cross branch (+9 lines, validated).

Terms

  • BBoxStrategy (lucene/spatial-extras, @lucene.experimental): indexes a spatial4j rectangle as four coordinates plus a dateline flag. makeQuery matches documents; makeOverlapRatioValueSource(queryBox, p) returns a DoubleValuesSource for ranking, sorting or boosting by overlap.
  • Overlap ratio: the class javadoc defines intersectionArea as "the area of the intersection between the query and target envelopes", queryRatio = intersectionArea / queryArea, targetRatio = intersectionArea / targetArea, and score = queryRatio * p + targetRatio * (1 - p) (BBoxOverlapRatioValueSource.java:28-48). Areas are width x height in degrees.
  • Dateline-crossing box: spatial4j rect(minX, maxX, minY, maxY) with minX > maxX covers [minX, 180] + [-180, maxX]. Its uncovered gap is (maxX, minX).

What happens, step by step (main a9779ef3c193, lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/)

  1. makeOverlapRatioValueSource builds new BBoxOverlapRatioValueSource(makeShapeValueSource(), ctx.isGeo(), queryBox, p, 0.0) (BBoxStrategy.java:232-235). For each document, the value source reads the document's box from doc values and calls score.
  2. score (BBoxOverlapRatioValueSource.java:146-265) computes the intersection height, then the width. When both boxes cross or both do not (:163), it takes left = max(a.minX, b.minX) and right = min(a.maxX, b.maxX) (:165-166). For both crossing it sets width = right - left + 360 (:180-181): the arc from left east across the dateline to right.
  3. If the two gaps are strictly separated (touching gaps leave a second piece of width 0) and the latitudes overlap, each box also covers the arc between the gaps, away from the dateline: [a.minX, b.maxX] when b.maxX > a.minX, or [b.minX, a.maxX] when a.maxX > b.minX (at most one holds). That arc is never added.
  4. When only one box crosses (:183-203), the code adds a west and an east piece (:189-195), so the same geometry moved off the dateline is scored in full.
  5. Every later step is non-decreasing in the width (calcArea, :268-270; the ratios, :208-232; the convex combination, :236-238), so this defect only ever lowers the score. explain() prints the same short width (:249).

Examples (executed on main)

querydocumentexpectedactual
rect(10, -170, -10, 10)rect(170, 20, -10, 10)overlap 20 + 10 = 30, score 0.15476190476190477width 20, 0.10317460317460317
rect(-20, 160, -10, 10) (the pair moved 30° west)rect(140, -10, -10, 10) (only the document crosses)0.154761904761904770.15476190476190477
rect(10, -170, -10, 10), sorted descendingA = rect(170, 20, ...), B = rect(-145, 35, ...) (overlap 10..35, no crossing)A (0.1548) before B (0.1389)B (0.1389) before A (0.1032)
rect(60, -60, -30, 30) (independent reproducer)rect(140, 100, -30, 30)overlap 160 + 40 = 200, 0.7291666666666667width 160, 0.5833333333333333
Exposure and likelihood369 words
  • Who is hit. Users of makeOverlapRatioValueSource (or BBoxOverlapRatioValueSource directly) in a geo context. BBoxStrategy is in the opt-in spatial-extras module and is @lucene.experimental. Within that API nothing special is needed: the default spatial4j SpatialContext.GEO, BBoxStrategy's default field type, the default codec and a plain IndexWriterConfig (executed: a probe with new IndexWriterConfig() on main, assertions on and off, gives the same value). Downstream, Solr's BBoxField exposes the class as score=overlapRatio ("a relevancy sort/boost"), and geo-catalogue software asks for that score; Elasticsearch and OpenSearch do not use it (read-only code search, not run).
  • What it takes. Both boxes cross the dateline, their latitudes overlap and their uncovered gaps are strictly separated (disjoint and not touching). That needs their widths to add up to more than 360° (necessary, not sufficient): for example a map view wider than half the globe centred on the Pacific, against a near-global dataset extent that also wraps. Deterministic, the same with assertions on and off.
  • What goes wrong. The document's score, sort value and explanation are too low by the dropped piece's share, so it ranks below worse matches. For ordinary boxes at p = 0.5 the deficits shown here are 0.052 (the example) and 0.146 (the independent reproducer's pair). The largest deficit on the hunt's probe grid, 0.69 (0.069 instead of 0.759), is a degenerate pair: two zero-height boxes (horizontal lines at latitude -90, scored as a ratio of widths) at p = 0.
  • Not affected. Which documents BBoxStrategy's spatial predicates match: BBoxStrategy.makeQuery never uses this value source (executed: the Intersects query still matches). A filter that an application builds on this score is affected (executed: with a threshold of 0.12 on the example pair, a positive filter drops the document and a deny rule lets it through). Also not affected: the index, deletions, and other documents' scores (a document's value depends only on its own box and the query box). This defect does not throw and cannot push a score above its exact value (so not above 1); the work per document does not change.
  • How likely. Rare: two crossing boxes that together span more than 360° are uncommon, and no prior report was found in the documented searches, 12 years after 4.10.0.

Minimal repro

The test shows that BBoxStrategy.makeOverlapRatioValueSource counts only the dateline piece when two dateline-crossing boxes overlap twice (query rect(10,-170,-10,10), document rect(170,20,-10,10), true overlap 20 + 10 = 30 degrees).

TestBBoxOverlapRatioBothCrossDateline.java (JUnit test)55 lines
/*
 * 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 org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.search.DoubleValues;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.shape.Rectangle;

public class TestBBoxOverlapRatioBothCrossDateline extends LuceneTestCase {

  public void testTwoPieceOverlap() throws Exception {
    SpatialContext ctx = SpatialContext.GEO;
    BBoxStrategy strategy = BBoxStrategy.newInstance(ctx, "bbox");
    Rectangle query = ctx.getShapeFactory().rect(10, -170, -10, 10);
    Rectangle target = ctx.getShapeFactory().rect(170, 20, -10, 10);
    // overlap = 170..-170 (20 deg) + 10..20 (10 deg) = 30 x 20; query 180 x 20, target 210 x 20
    double expected = (30.0 * 20) / (180 * 20) * 0.5 + (30.0 * 20) / (210 * 20) * 0.5;
    try (Directory dir = newDirectory()) {
      try (IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig())) {
        Document doc = new Document();
        for (Field f : strategy.createIndexableFields(target)) {
          doc.add(f);
        }
        writer.addDocument(doc);
      }
      try (DirectoryReader reader = DirectoryReader.open(dir)) {
        DoubleValues values =
            strategy.makeOverlapRatioValueSource(query, 0.5).getValues(reader.leaves().get(0), null);
        assertTrue(values.advanceExact(0));
        assertEquals("overlap ratio, intersection 30 x 20", expected, values.doubleValue(), 1e-9);
      }
    }
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-369/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble   (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/spatial-extras/build/libs/lucene-spatial-extras-$V.jar
CP=$CP:$LUCENE/lucene/spatial3d/build/libs/lucene-spatial3d-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar   # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar   # JUnit Platform console launcher (Maven Central)

javac -d classes -cp "$CP" $(find src -name '*.java')

# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
     -cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
     execute --select-class org.apache.lucene.spatial.bbox.TestBBoxOverlapRatioBothCrossDateline
# the module(s) spatial-extras also need their third-party runtime jars
Expected vs actual
Expected: the documented score 600/3600*0.5 + 600/4200*0.5 = 0.15476190476190477; actual (-ea and -da): 0.10317460317460317 (width 20, from right - left + 360, BBoxOverlapRatioValueSource.java:181).

All files of this repro: repro/LFV-369/

More files, environment, full output39 words

Environment

luceneLucene main e1faeba08bde (2026-11-02)
jdk25.0.3+9-LTS

Observed output

# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
    => java.lang.AssertionError: overlap ratio, intersection 30 x 20 expected:<0.15476190476190477> but was:<0.10317460317460317>
       org.junit.Assert.fail(Assert.java:89)
       org.junit.Assert.failNotEquals(Assert.java:835)
       org.junit.Assert.assertEquals(Assert.java:555)
       org.apache.lucene.spatial.bbox.TestBBoxOverlapRatioBothCrossDateline.testTwoPieceOverlap(TestBBoxOverlapRatioBothCrossDateline.java:51)

Why it looks unintended

  • The javadoc defines intersectionArea as "the area of the intersection between the query and target envelopes"; isGeo means "dateline issues should be attended to".
  • The same method adds both pieces when only one box crosses; the upstream "wrap" test (0.2794117) expects that sum.
  • Moving both boxes 30° west cannot change their overlap, yet the score rises from 0.1032 to 0.1548.
The full argument256 words
  • The contract. BBoxOverlapRatioValueSource.java:35-48 defines intersectionArea as the intersection's area and the score from it; the constructor documents isGeo as "True if ctx.isGeo() and thus dateline issues should be attended to" (:85).
  • The sibling branch and its test. When one box crosses, the code sums a west and an east piece (:183-203). TestBBoxStrategy#testOverlapRatio, case "wrap", expects 0.2794117 (TestBBoxStrategy.java:258-259): query [0, 40] against document rect(30, 10) overlaps in [30, 40] + [0, 10] = 20°, so 800/1600 x 0.5 + 800/13600 x 0.5.
  • Rotation invariance. The same pair scores 0.15476 moved 30° west and 0.10317 in place; a credibility audit's metamorphic test, which uses no hand-computed value, fails the same way and passes with the fix.
  • The computed width means nothing else. 20 is neither the true overlap (30) nor the width of either enclosing envelope (180 or 210).
  • No test pins the current value. Swept over all its (SHIFT, nudge) combinations, the upstream "wrap" case reaches the both-cross branch only twice, both times with a second piece of width 0; stock and fixed code agree there.
  • History. The line is a leftover of the 2012 port (LUCENE-4175, width = right + 360.0 - left in the unreleased AreaSimilarity). LUCENE-5714/5779 (2014) rewrote the one-crossing branch to sum both pieces and kept this one; their threads describe the work as fixing "a variety of dateline related bugs when computing intersection width & height" (#6841). None of the 35 comments on #5247, #6776 and #6841 discusses two crossing boxes that overlap twice.
The devil's-advocate check239 words

The strongest case for intended behaviour (the hunt's intended-behaviour checker; a read-only cross-model reviewer, OpenAI GPT-5.6, which did not run code, rated it weak): the class is an @lucene.experimental relevance heuristic whose javadoc says it works "as envelope on envelope (rect on rect) overlays rather than complex polygon on complex polygon overlays" (:25-26), so "the intersection between the query and target envelopes" could mean one envelope rather than a two-piece region; the formula was already in Lucene's 2012 port, and the class says it was "Originally based on" Geoportal's SpatialRankingValueSource (:61-63); areas are planar degrees anyway (// TODO option to compute geodetic area, :81); the trigger is unusual; and no prior report was found in 12 years.

Why it fails: the same method already counts a two-piece overlap in full when one box crosses, and the upstream "wrap" assertion depends on it, so the single-envelope reading contradicts the class's own code and test. Under that reading the answer would be an envelope 180° or 210° wide, not 20°. The javadoc says dateline issues are attended to in geo mode, yet the result depends on where the dateline falls. The origin is no argument for keeping the formula: the same javadoc says Lucene's version was "modified quite a bit" and that Geoportal's algorithm "had dateline bugs" (:63-65). The planar approximation is applied consistently elsewhere and is not what goes wrong here.

Suggested fix

Recommended Add the second overlap piece in the both-cross branch

In the both-cross branch, also add the overlap away from the dateline: max(0, b.maxX - a.minX) + max(0, a.maxX - b.minX); at most one term is positive, so nothing is counted twice (+9 lines). The repro passes 3 of 3 with assertions on and off, and all 120 spatial-extras tests pass.

         } else { // both cross
+          // the overlap that contains the dateline
           width = right - left + 360;
+          // the two boxes can also overlap away from the dateline, when each one covers the gap the
+          // other leaves (at most one of these two terms is positive)
+          if (b.getMaxX() > a.getMinX()) {
+            width += b.getMaxX() - a.getMinX();
+          }
+          if (a.getMaxX() > b.getMinX()) {
+            width += a.getMaxX() - b.getMinX();
+          }
         }
All options and trade-offs4 options · 651 words

Option A was applied in a scratch worktree at a9779ef3c193 and run through gradle (seed DEADBEEF), and checked with the same class shadowed onto the main jars; the hunt's equivalent version also cleared its exhaustive checks. B, C and D were not run.

Add the second overlap piece in the both-cross branch (validated by the checks) (recommended)

After width = right - left + 360; (the piece across the dateline), add b.maxX - a.minX when it is positive and a.maxX - b.minX when it is positive. For two crossing boxes (a.maxX < a.minX, b.maxX < b.minX) both cannot hold, since together they would give b.maxX > b.maxX. The added arc lies between the two gaps, disjoint from the dateline piece. When the gaps overlap, neither term is positive and the result is unchanged.

         } else { // both cross
+          // the overlap that contains the dateline
           width = right - left + 360;
+          // the two boxes can also overlap away from the dateline, when each one covers the gap the
+          // other leaves (at most one of these two terms is positive)
+          if (b.getMaxX() > a.getMinX()) {
+            width += b.getMaxX() - a.getMinX();
+          }
+          if (a.getMaxX() > b.getMinX()) {
+            width += a.getMaxX() - b.getMinX();
+          }
         }

Trade-offs

  • Behaviour change: only pairs where both boxes cross the dateline and their gaps are disjoint score higher, up to the exact value; every other pair keeps its score, so rankings change only for those documents.
  • Cost: two comparisons per document that reaches this branch; no benchmark was run. No format or API change.

What the checks validated

  • Stock main a9779ef3c193 (gradle, seed DEADBEEF): the repro fails 2 of 3 with assertions on and off (the control passes); TestBBoxStrategy passes (12 tests, 1 skipped by its own @Ignore; testOverlapRatio with the "wrap" case runs); all of :lucene:spatial-extras:test: 120 tests, the repro's 2 failures, 5 skipped.
  • With the fix: the repro passes 3 of 3 with assertions on and off; TestBBoxStrategy passes; all of :lucene:spatial-extras:test (120 tests, 5 skipped, the repro class included) passes with 0 failures; ecjLintMain, ecjLintTest and checkGoogleJavaFormat pass for spatial-extras; fix.diff applies cleanly to a clean tree.
  • With the fixed class shadowed onto the main jars (classpath runner): 3 of 3 pass with -ea and -da.
  • The hunt's equivalent fix (two one-line ifs) took the exhaustive check from 154,000 mismatches to 0 (12,400,200 checks) and the security probe's undercount counter from 67,200 to 0 (that probe's own two assertions expect the bug, so on the fixed code they fail by design; its counters read under=0, over=0), kept the upstream "wrap" assertion passing, and, applied to the 4.10.0 class, made the 4.10.0 port pass.

Compute the both-cross width from the two gaps

width = 360 - gapA - gapB + overlap(gapA, gapB), with gap = minX - maxX: everything except what either box leaves uncovered.

Trade-offs

Not run in Lucene. Algebraically the same width as A, but not bit for bit: in double arithmetic it rounds differently, also for pairs A leaves unchanged (a seeded sample of 1,000,000 random both-cross pairs: 285,412 widths differed from A's, 168,407 of them one-piece pairs whose stock value A keeps exactly, by at most 1.1e-13 degrees). It needs its own interval-overlap code and its own tests.

One arc-intersection helper for all branches

Treat each box as one or two longitude intervals and sum the pairwise overlaps, replacing the three-way case analysis.

Trade-offs

Not run. Simpler to reason about, but it rewrites the tested one-crossing branch and the "both adjacent to dateline" special case (:170-173), so it needs more review.

Document the limitation

Say in the javadoc that, when both boxes cross the dateline, only the overlap across the dateline is counted.

Trade-offs

Not recommended. Keeps the wrong rankings and the dependence on where the dateline falls.

Downstream note

Solr's BBoxField exposes this score as score=overlapRatio for relevancy sort and boost, so Solr users who rank wide dateline-crossing extents get the same too-low scores.

How it was found

Bounded-exhaustive checking of the real jar against the class-javadoc formula: every ordered pair of 1,245 geo boxes, 4 proportions and 2 minimum side lengths, 12,400,200 checks. 154,000 disagreed, every one a two-piece overlap; the fix, which changes only the both-cross branch, takes them to 0. A public-API JUnit then confirmed it.

The method in full192 words

Method: semi-formal, bounded-exhaustive checking against exact semantics (hunt unit on spatial-extras, 2026-10-26, main ea57360912ed).

  • A harness built 1,245 distinct geo boxes (longitudes from {-180, -170, -100, -90, 0, 10, 90, 100, 170, 180}, latitudes from {-90, -10, 0, 10, 90}) and scored every ordered pair with BBoxOverlapRatioValueSource for queryTargetProportion {0, 0.25, 0.5, 1} and minSideLength {0, 0.1}, with assertions on and off.
  • The reference was an independent evaluation of the class-javadoc formula, with the intersection measured as arcs on the longitude circle.
  • 154,000 of 12,400,200 checks disagreed, all two-piece intersections (counted by the harness); 0 NaN, 0 scores outside [0, 1], 0 assertion failures. With the fix sketch, which changes only the both-cross branch, 0 disagreed, so every mismatch was a pair of two crossing boxes; a later probe through the public indexing path, over the same boxes and proportions with the default minimum side length 0, counts that directly: 0 undercounts outside both-cross, 0 overcounts.
  • The finder then wrote a JUnit on the public indexing path (after a first cross-model review objected to a repro that called the protected score() directly) and confirmed the failure on main, 10.5.2 and 9.12.3.

Prior reports, history and checks

Prior reports

No prior report found (150 logged queries, 2026-10-26): GitHub (Lucene, Solr, Elasticsearch, OpenSearch, spatial4j, downstream users), JIRA, the Lucene and Solr lists, the web, and the files of all 411 open Lucene PRs. Closest: LUCENE-5714 and LUCENE-5779 (2014), which fixed other dateline width bugs in this method.

Searches5 searches

18 curated hits, none the same bug. #6776 / LUCENE-5714 and #6841 / LUCENE-5779 created the class and its javadoc and fixed other dateline width bugs, but kept this formula; #5247 / LUCENE-4175 is the formula's origin. SOLR-6183 exposes the score in Solr as score=overlapRatio; apache/solr#4145 (SOLR-18111) is an open exploratory PR to remove BBoxField in a future Solr major. The only open PR touching the bbox package, #12295, changes casts in BBoxStrategy.makeQuery only. Limits: keyword search only; GitHub search does not index diffs; the original Geoportal code has no searchable tracker; private reports are not visible.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and code (apache/lucene, apache/solr, Elasticsearch, OpenSearch, spatial4j, Lucene.NET, GeoBlacklight, all of GitHub)2026-10-26a scripted prior-report search (20 issue/PR, 3 commit, 4 downstream-code queries) plus 65 read-only GraphQL searches: class names, overlapRatio, "overlap ratio", bbox with dateline/antimeridian, "both cross", getCrossesDateLine, geoportal, SpatialRankingValueSourceno report; #6776, #6841 and #5247 are precedent; open BBoxStrategy issues #5439, #6864, #6866, #7989 are about other topics
Open apache/lucene PRs2026-10-26the changed files of all 411 open PRs (the 3 with more than 100 files re-listed in full)only #12295 touches the bbox package (casts in makeQuery); none touches BBoxOverlapRatioValueSource.java
JIRA (LUCENE, SOLR)2026-10-266 queries through the tool and 7 JQL queries; SOLR-6183, SOLR-6784, SOLR-18111, LUCENE-4373, LUCENE-6931 read in fullno report; SOLR-6183 (score=overlapRatio) and SOLR-6784 (an NPE) are unrelated
Mailing lists2026-10-2620 queries over dev@, java-user@, issues@ and commits@ lucene, plus 14 over the Solr and Lucene lists; 19 messages readusage advice for overlapRatio sorting; nobody reports wrong scores
Web and git history2026-10-268 web searches; GitHub commits API for the file and the bbox package; git log --follow, -S on the both-cross formulanothing on this bug; the file is unchanged since the 2021 reformat

History

Introduced with the class in 4.10.0 by LUCENE-5714/5779 (7e0aff8dabc4, 2014-07-08); the formula itself dates from the 2012 LUCENE-4175 port. Executed on 15 releases from 4.10.0 to 10.5.2 and on main: all affected; 4.10.0 with the fix passes. Not a regression; not fixed upstream.

Versions tested6 rows

Introduced by 7e0aff8dabc4 on trunk (2014-07-08) and its branch_4x backport c608265684cf (2014-07-09, the copy that shipped first), "LUCENE-5714, LUCENE-5779: Enhance BBoxStrategy & Overlap similarity. Configurable docValues / index usage. Add new ShapeAreaValueSource." by @dsmiley; JIRA era, no pull request (LUCENE-5714 is #6776, LUCENE-5779 is #6841). It created BBoxOverlapRatioValueSource with the documented intersection-area formula and rewrote the one-crossing branch to sum both pieces, but kept the both-cross formula. That formula came from 26d3f1f813ea (2012-06-28, "LUCENE-4175: adding bbox strategy", @ryantxu, #5247), whose unreleased AreaSimilarity had width = right + 360.0 - left. First released in 4.10.0 (CHANGES: "Index and search rectangles with spatial BBoxSpatialStrategy using most predicates. Sort documents by relative overlap of query areas ..."). Later commits on the width block are the LUCENE-6446 explanations (9cc27ca4205, 2015) and the LUCENE-9570 reformat (2cbf261032d, 2021). All 103 release tags in the history clone that contain the class have the line unchanged (8.8.2 to 8.11.3 are not in the clone; 8.11.4 was executed), so every release since 4.10.0 is very likely affected, but only the releases listed below were executed. Upstream main 20ced71c1e6 (2026-10-27), branch_10x and branch_10_6 have the same file as a9779ef3c193, so 10.6.0 is very likely affected (not executed).

Versions and branches executed, with results
Version or commitVerdictResult
main a9779ef3c193 affectedthis repro 2 of 3 fail with -ea and -da (gradle and classpath runner); the hunt's JUnit 1 of 1
10.5.2, 9.12.3 affectedthis repro 2 of 3 fail (-ea), the same values; the finder's JUnit fails with -ea and -da, and the reproducer's JUnit fails the same way
9.0.0, 9.1.0, 10.0.0 affectedthe hunt's JUnit fails 1 of 1 (9.0.0 with only the LuceneTestCase import changed)
4.10.0, 4.10.4, 5.0.0, 5.5.5, 6.0.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4 main() ports0.10317460317460317 instead of 0.15476190476190477; a one-crossing two-piece control scores exactly (4.10.0 also with -da)
4.10.0 + fix fix checkthe port passes with the two added terms shadowed into the 4.10.0 class
4.9.0, 4.9.1 no APIBBoxStrategy does not exist; the port fails to compile only on that class

Independent checks

An independent reproducer wrote its own tests (4 of 5 fail; its grid sweep found 660 of 660 both-cross two-piece pairs wrong and none outside), an intended-behaviour devil's advocate (real), read-only cross-model reviews (root cause agreed), an adjudication (confirmed, high confidence), and re-runs on main at the registration check and in a credibility audit.

Checks and verdicts249 words

The executed checks ran on main ea57360912ed (identical spatial-extras) unless stated, some also on 10.5.2 and 9.12.3, with assertions on and off where stated; the main-refresh step re-ran the finder's JUnit on a9779ef3c193 (still fails). The cross-model reviews could only read code, so their verdicts on reality are procedural.

Each independent check and its verdict
CheckVerdictWhat it found
Independent reproducer realOwn public-API JUnit from the claim alone: 4 of 5 fail on main (-ea, -da), 10.5.2 and 9.12.3; the one-crossing control passes; a sorted search ranks the wrong document first; 5 of 5 pass with the fix.
Intended-behaviour devil's advocate real (high that it is not intended)Built the single-envelope case and rejected it; showed the width 20 matches no alternative meaning, and that no upstream assertion pins the buggy value.
Cross-model reviews (OpenAI GPT-5.6, read-only) intended lens real (medium); refute lens not real (low, procedural)The reviewer could not run code; the lenses agreed on the root cause and that the inputs are in contract, and rated the intended-behaviour argument weak. Each separate re-run of the repro failed 1 of 1.
Adjudication confirmed, high confidenceRe-ran the finder's JUnit (-ea, -da) and the independent reproduction's (4 of 5 fail, control passes); hand-checked 20 + 10 = 30° against the code's 20.
Registration check and credibility audit keepRe-runs on main with fresh seeds fail as before; a new rotation test with no hand-computed value fails and passes with the fix.

Diagram file

The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,463 words

Finding record

LFV-369, snapshot of 2026-11-06

Registry title
BBoxStrategy's overlap-ratio relevance (BBoxOverlapRatioValueSource) undercounts the overlap of two dateline-crossing boxes that intersect in two longitude pieces, so the score is too low
Area
spatial-extras / BBoxStrategy overlap-ratio relevance (BBoxOverlapRatioValueSource.score: intersection width when both boxes cross the dateline)
Priority
Low, rank 437: A spatial-extras overlap-ratio score comes out too low, which affects ranking only and only for pairs of very wide boxes that both cross the dateline.
Impact class
silent wrong results
Impact note
Applications that rank or sort by BBoxStrategy's overlap-ratio relevance (makeOverlapRatioValueSource in the opt-in spatial-extras module; Solr's BBoxField exposes the same class as score=overlapRatio, by a read-only code search) in the default geo context get a score that is too low, so the document ranks too low, whenever the query box and the document box both cross the dateline and the longitude stretches they leave uncovered do not overlap, which needs their widths to add up to more than 360 degrees; default BBoxStrategy, codec and IndexWriterConfig, one document suffices, -ea and -da, every release since 4.10.0; matching, filtering and the index are unaffected, the score never goes up and nothing throws.
Summary
You index a dateline-crossing rectangle with BBoxStrategy in the default geo context and score it with makeOverlapRatioValueSource and a query rectangle that also crosses the dateline, and you expect the documented score intersectionArea/queryArea*p + intersectionArea/targetArea*(1-p), where intersectionArea is 'the area of the intersection between the query and target envelopes'. When the two boxes wrap far enough round the globe that they overlap twice, once across the dateline and once on the far side (query rect(10,-170,-10,10) and document rect(170,20,-10,10) share 170..-170 across the dateline, 20 degrees, and 10..20, 10 degrees), the score counts only the piece across the dateline: 0.10317460317460317 instead of 0.15476190476190477 at p = 0.5. The both-cross branch computes the width as right - left + 360, which is only that piece, while the branch for one crossing box already adds up both pieces.
Introduced
7e0aff8dabc4c3780f1e16a515646483cf92c2cc (trunk, svn r1608793) and its branch_4x backport c608265684cf73f50bf89bfbf252b7080769ee22 (svn r1608987, the copy that shipped first, in 4.10.0), 'LUCENE-5714, LUCENE-5779: Enhance BBoxStrategy & Overlap similarity. Configurable docValues / index usage. Add new ShapeAreaValueSource.', by dsmiley; no pull request (SVN + JIRA era; LUCENE-5714 is apache/lucene#6776, LUCENE-5779 is #6841). It created BBoxOverlapRatioValueSource with the documented intersection-area formula and rewrote the one-crossing branch to sum both pieces, but kept the both-cross single-arc formula. Origin of the formula: 26d3f1f813ea234ef443f237a932b187d65709a9 (svn r1354841, 2012-06-28, 'LUCENE-4175: adding bbox strategy', by ryantxu, #5247), whose unreleased trunk class AreaSimilarity had 'width = right + 360.0 - left'. [read: git log --follow, git blame, git log -S on both spellings and git log -L on the width block in the history clone; exec: 4.9.0 and 4.9.1 have no such API, 4.10.0 fails and 4.10.0 with the two-line fix shadowed passes]. Not a regression: the line is unchanged since (later commits on the block: LUCENE-6446 explanations and the LUCENE-9570 reformat) (2014-07-08 (trunk 7e0aff8dabc4); 2014-07-09 (branch_4x c608265684cf); the formula itself dates from 26d3f1f813ea, 2012-06-28 (unreleased trunk AreaSimilarity))
First affected version
4.10.0 [exec] (tag 2014-09-02, the first release with BBoxStrategy and BBoxOverlapRatioValueSource; fails with -ea and -da); 4.9.0 and 4.9.1 have no such API [exec: the port fails to compile only on the missing BBoxStrategy class]
Affected versions
Every release from 4.10.0 to 10.6.0 (10.6.0 by reading) and main ea57360912ed; not fixed upstream. [exec] the same pair scores 0.10317460317460317 instead of 0.15476190476190477 on 4.10.0 (-ea and -da), 4.10.4, 5.0.0, 5.5.5, 6.0.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.1.0, 9.12.3 (-ea and -da), 10.0.0, 10.5.2 (-ea and -da) and main (-ea and -da) (follow-up; 4.10.0 to 8.11.4 through main() ports of the same rectangles, public indexing path and oracle, each of which also scores a one-crossing two-piece control exactly; 8.11.4 and 9.0.0 through the JUnit with only the LuceneTestCase import changed; 9.1.0 on through the unchanged JUnit); the independent reproduction's own JUnit fails 4 of 5 on main (-ea and -da), 10.5.2 and 9.12.3. [read] all 103 release tags in the history clone that contain the class (4.10.0 to 8.8.1, 9.0.0 to 10.5.2) have the both-cross line unchanged; 8.8.2 to 8.11.3 are not in the clone (8.11.4 was executed); branch_10_6 has a file identical to main (not executed, so 10.6.0 is very likely affected). All runs on aarch64 with JDK 25
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
semi-formal: exhaustive check
Upstream and prior reports
none found (2026-10-26, read-only, the prior-report record, verdict new: 150 logged queries: a scripted prior-report search (20 GitHub issue/PR, 3 commit, 4 downstream, 20 mailing-list over dev@, java-user@, issues@ and commits@, 6 JIRA, 6 web), 65 read-only GitHub GraphQL searches (apache/lucene, apache/solr, Elasticsearch, OpenSearch, spatial4j, Lucene.NET, GeoBlacklight and all of GitHub), 7 JIRA REST queries over LUCENE and SOLR, 14 direct mailing-list searches including the Solr lists, 2 more web searches, the GitHub commits API, a snapshot of all 411 open apache/lucene PRs with their files, and the local git history; plus the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's earlier searches); no hit is the same bug. Not fixed upstream: the file is unchanged since the 2021 reformat and no open PR touches it (the only open PR touching the bbox package, #12295, changes BBoxStrategy.makeQuery casts only). Related items, not reports of this bug: #6776 / LUCENE-5714 and #6841 / LUCENE-5779 (2014; created the class and its intersection-area javadoc and fixed other dateline width bugs, but kept this formula), #5247 / LUCENE-4175 (2012, origin of the formula), SOLR-6183 (exposes the score in Solr as score=overlapRatio), locationtech/spatial4j#85 (relate() of zero-width boxes at +/-180, not the overlap width), apache/solr#4145 (SOLR-18111, an open exploratory PR to remove BBoxField in a future Solr major).
Lucene commit tested
main a9779ef3c193 (commit of 2026-10-26): the repro JUnit through gradle in a clean scratch worktree, stock and with the fix, assertions on and off, plus TestBBoxStrategy and all of lucene/spatial-extras on stock and with the fix, and the module's lint and format checks (2026-10-28); through the hunt's classpath runner, the same JUnit on main (-ea, -da, fix shadowed), 10.5.2 and 9.12.3 (-ea). Hunt runs on main ea57360912ed (identical lucene/spatial-extras) and on 15 releases from 4.10.0 to 10.5.2. OpenJDK 25.0.3, Linux aarch64.

Lucene and upstream sources

Finding record
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java:24-67 (class javadoc: intersectionArea is 'the area of the intersection between the query and target envelopes' at :35-36, score formula at :37-48, Geoportal 'dateline bugs' note, @lucene.experimental at :67), :85 ('isGeo True if ctx.isGeo() and thus dateline issues should be attended to'), :163-181 (both-cross branch: left = max(minX) at :165, right = min(maxX) at :166, width = right - left + 360 at :181), :183-203 (one-crossing branch sums both pieces), :207-238 (ratios and score)
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java:47-71 (class javadoc: custom overlap relevancy, 'Uses the DocValues API for any sorting / relevancy', @lucene.experimental) and :228-235 (makeOverlapRatioValueSource passes ctx.isGeo() and minSideLength 0.0)
  • lucene/spatial-extras/src/test/org/apache/lucene/spatial/bbox/TestBBoxStrategy.java testOverlapRatio ('wrap' case, 0.2794117, a two-piece sum through the one-crossing branch)
The finding in 30 seconds
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java:24-67 (at a9779ef3c193) (class javadoc, @lucene.experimental), :146-265 (score; width 158-204, both-cross 180-181, one-crossing 183-203), :249 (explain width), :268-270 (calcArea)
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java:63-71 (at a9779ef3c193) (implementation note, @lucene.experimental), :232-235 (makeOverlapRatioValueSource)
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxSimilarityValueSource.java:66-84 (at a9779ef3c193) (getValues calls score per document)
Who is affected
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java:71 (at a9779ef3c193) (@lucene.experimental), :121 (newInstance, DEFAULT_FIELDTYPE), :232-235, :254-359 (makeQuery)
Why it looks unintended
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java:25-26, (at a9779ef3c193) :35-48, :61-65 (Geoportal origin, "had dateline bugs"), :81, :85, :180-181, :183-203
  • lucene/spatial-extras/src/test/org/apache/lucene/spatial/bbox/TestBBoxStrategy.java:227-259 (at a9779ef3c193) (testOverlapRatio, 'wrap' 0.2794117)
History
  • lucene/CHANGES.txt:11840-11842 (at a9779ef3c193) (under 4.10.0, :11829)
  • a Lucene clone: git diff --quiet a9779ef3c193 against origin/main 20ced71c1e6, origin/branch_10x 1309534296e and origin/branch_10_6 2763db11b0a for BBoxOverlapRatioValueSource.java (identical), 2026-10-28

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Made in parallel with this doc from the same example (query rect(10, -170, -10, 10), document rect(170, 20, -10, 10), p = 0.5), whose values come from an executed probe on main a9779ef3c193 with assertions on and off and with the fix. The diagram's fix panel ("the hunt's diff") and the card's "two added lines" show the hunt's version of fix A: two one-line ifs with their own comments. The logic is the same as this doc's fix A and both were executed, but the text differs. On the card, A is the example document and B is rect(-145, 35, -10, 10).

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.