A box is stored as its west edge minX and east edge maxX; a box across the dateline has minX > maxX (170 to -170), and the flag should say exactly that. Geo3d's flag is maxX > 0 && minX < 0: false for 170..-170, true for -10..10. Both strategies choose their longitude ranges from the flag.
PointVectorStrategy builds from it, and the xdl flags BBoxStrategy stores for the two indexed boxes. Select the still to open it at full size. Generated with AI, checked against the code.- You use the Geo3d spatial context (
Geo3dSpatialContextFactory) withPointVectorStrategyand index four points: A (175, 0), B (-178, 1), C (100, 0) and D (5, 0). - You search the 20-degree box across the dateline,
rect(170, -170, -10, 10), and expect A and B. You get 0 hits: the query is the empty rangex:[170.0 TO -170.0]. A 5-degree circle around (180, 0) also finds 0 instead of 2. - You search the box around longitude 0,
rect(-10, 10, -10, 10), and expect D only. You get all 4 points: the query becomesx <= 10 OR x >= -10, which is every longitude. - Why:
Geo3dRectangleShape.getCrossesDateLine()returnsmaxX > 0 && minX < 0("spans longitude 0") instead ofminX > maxX("crosses the dateline"), and both strategies pick their longitude ranges from it.BBoxStrategyalso writes it into the index (thexdlterm): the box 170..-170 is missed by the query 175..176 inside it, and the box -10..10 matches a query at 100..110. - No error; the default spatial4j context answers 2, 1 and 2 hits. Every release since 7.1.0 (2017) has it, and
BBoxStrategyindexes keep the wrong flag until they are reindexed.
Applications that pair Geo3d shapes (the opt-in Geo3dSpatialContextFactory, or spatial3d shapes wrapped in Geo3dShape) with PointVectorStrategy or BBoxStrategy. There, every box that crosses the dateline or spans longitude 0 is affected, as a query or as an indexed document. RecursivePrefixTreeStrategy and the default spatial4j context are not affected.
Mechanism, terms and numbers851 words
In the opt-in Geo3d spatial context (Geo3dSpatialContextFactory, @lucene.experimental), Geo3dRectangleShape.getCrossesDateLine() returns getMaxX() > 0 && getMinX() < 0 ("the box spans longitude 0") instead of minX > maxX ("the box crosses the dateline"), the definition in spatial4j's own RectangleImpl, which the same class's getWidth() follows. PointVectorStrategy and BBoxStrategy use the flag to decide whether minX..maxX wraps, so they miss documents in dateline boxes and circles near longitude 180 and match far-away documents for boxes around longitude 0; BBoxStrategy also writes the wrong flag into the index. No recorded run threw, and CheckIndex found the probe indexes clean. It came in with LUCENE-7951 in 7.1.0 (2017) and is in every release since and on main; the one-line fix return getMinX() > getMaxX(); passes every JUnit repro and the module's tests.
- Spatial context: spatial4j's
SpatialContextbuilds and relates shapes.Geo3dSpatialContextFactory(opt-in,@lucene.experimental) makes one whose shapes wrap spatial3d (Geo3d) shapes. Its rectangles areGeo3dRectangleShape, and every Geo3d shape'sgetBoundingBox()is one too. - Dateline box: a
Rectanglekeeps its west edge inminXand its east edge inmaxX. A box that crosses the -180/180 meridian hasminX > maxX:rect(170, -170, ...)is the 20 degrees from 170 eastwards to -170.getCrossesDateLine()says whether that is the case. PointVectorStrategyandBBoxStrategy: two non-default@lucene.experimentalstrategies. One indexes a point as two numbers (x, y), the other a box as four numbers plus a dateline flag, and both search with numeric range queries. They need the flag to know whetherminX..maxXis one range or wraps around.
Geo3dRectangleShape.getCrossesDateLine()(Geo3dRectangleShape.java:152-155) returnsgetMaxX() > 0 && getMinX() < 0: true when the box straddles longitude 0, false for a box across the dateline. spatial4j'sRectangleImplreturnsminX > maxX.- The same class's
getWidth()(:118-125) does wrap: it adds 360 whenmaxX - minXis negative, so 170..-170 is 20 degrees wide.Geo3dShapeFactory.rect(Geo3dShapeFactory.java:163-173) keepsminX > maxXfor such a box, andGeo3dShape.getBoundingBox()(Geo3dShape.java:107-118) returns aGeo3dRectangleShapefor every Geo3d shape. PointVectorStrategy.makeWithin(PointVectorStrategy.java:209-222) builds two longitude ranges,x <= maxXORx >= minX, when the flag is true, and one rangeminX <= x <= maxXotherwise. A circle query uses the circle's bounding box the same way and adds a distance check (:195-198).BBoxStrategywrites the flag into the index as thexdltermTorF(BBoxStrategy.java:205-207), and its query builders branch on the query's flag (:299,:382,:542). A document withxdl=Tis read as the two pieces[minX, 180]and[-180, maxX]. Intersects is "not disjoint" (:473-496).
- Query box 170..-170 (flag false):
PointVectorStrategysearchesx:[170.0 TO -170.0], an empty range, and finds nothing. The same happens for a circle around (180, 0), whose bounding box is 175..-175. - Query box -10..10 (flag true):
PointVectorStrategysearchesx <= 10 OR x >= -10, every longitude, and matches every point in the latitude band. - Indexed box 170..-170, stored with
xdl=F: read as the empty range 170..-170, soBBoxStrategymisses it for a query inside it. - Indexed box -10..10, stored with
xdl=T: read as[-10, 180]plus[-180, 10], the whole longitude range, so it matches queries anywhere in its latitude band. - The flag is wrong for every box that crosses the dateline (
minX > maxX: 170..-170, but also 10..5 or 10..0, whose edges have the same sign), and for every other box whose west edge is below 0 and east edge above 0. Only boxes that do not cross the dateline and lie on one side of longitude 0 (an edge exactly at 0 included) get the right flag. The full-world box -180..180 changes too: true today, false with the fix (see the fix trade-offs). BBoxStrategyalso getsIsWithin,ContainsandIsDisjointTowrong (security probe);IsEqualTocompares the four numbers and does not read the flag (BBoxStrategy.java:457-466).- Other readers of the flag get the wrong value too: Lucene's
HeatmapFacetCounterandBBoxOverlapRatioValueSource, and, in mixed use, spatial4j's ownGeoCircle.relateRectanglePhase2andJtsShapeFactory.getGeometryFrom. No wrong result was shown through them (a 32-case heatmap probe showed none), and none is claimed.
| call or query, Geo3d context | expected | main | fix |
|---|---|---|---|
flag of rect(170, -170, -10, 10) / rect(-10, 10, -10, 10) | true / false | false / true | true / false |
PointVectorStrategy, points A (175, 0), B (-178, 1), C (100, 0), D (5, 0): Intersects(rect(170, -170, -10, 10)) | A, B | none | A, B |
the same points: Intersects(rect(-10, 10, -10, 10)) | D | A, B, C, D | D |
the same points: Intersects(circle(180, 0, 5)) | A, B | none | A, B |
BBoxStrategy, boxes E = 170..-170 and W = -10..10 (latitude -10..10): Intersects(rect(175, 176, -1, 1)) | E | W | E |
the same boxes: Intersects(rect(100, 110, -5, 5)) | none | W | none |
The default spatial4j context (SpatialContext.GEO) gives the "expected" column on main. Assertions on and off give the same results.
Exposure and likelihood437 words
- Who is hit. Applications that pair Geo3d shapes with
PointVectorStrategyorBBoxStrategy(both@lucene.experimental):- the Geo3d spatial context (
Geo3dSpatialContextFactory,@lucene.experimental) with either strategy; - spatial3d shapes wrapped in the public
Geo3dShapeconstructor under the defaultSpatialContext.GEO, the 7.0.x usage pattern: from 7.1.0 on their bounding box is aGeo3dRectangleShape, soBBoxStrategyindexes them with the wrong flag (follow-up bisection test).
- the Geo3d spatial context (
- Not affected.
RecursivePrefixTreeStrategy(it does not read the flag), shapes from spatial4j's own factory, and Lucene's core geo types (LatLonPoint,LatLonShape,XYShape, spatial3d'sGeo3DPoint). - Ordinary data. Within the pairing it is deterministic: any box over the dateline (Fiji, the Bering Sea) or across longitude 0 (a box around London, over western Europe or West Africa), as a query or as an indexed document, WKT
ENVELOPEinput included. - Both directions. Missed documents (dateline boxes, circles near longitude 180) and extra documents (a box around longitude 0 matches everything in its latitude band: as a
PointVectorStrategyquery and as aBBoxStrategydocument). - How common, on random data (security probe: 2,000 random boxes and points in 9 segments, 1,500 random query boxes):
BBoxStrategyIntersects was wrong on 1,474 of 1,500 queries andPointVectorStrategyIntersects on 857 of 1,500; every mismatch involved a box that spans longitude 0 or crosses the dateline. A second run with assertions off and a different seed gave similar counts (1,483 and 877); SPHERE and WGS84 gave identical counts. Half of the query boxes were chosen to span longitude 0 or cross the dateline, so the shares show the effect is frequent in this synthetic workload, not how often real data hits it. - Cost side effect. A
PointVectorStrategycircle of radius 1 at (0.3, 0.3) has a bounding box from -0.70 to 1.30 with the flag true, so its prefilter is the whole latitude band: on 100,000 points it distance-checks 20,118 candidates instead of 110 (about 5.5 ms instead of 0.2-0.3 ms on a loaded host). The answer stays right, and the cost stays below a legitimate world-sized circle (about 21 ms). - Silent and persistent. No exception or hang in any recorded run, and
CheckIndexfound the probe indexes clean; other filter clauses and deletions still apply. ButBBoxStrategyindexes the wrongxdlflag, so existing indexes stay wrong after a fix until they are reindexed (in the probexdl=Twas set on exactly the boxes that span longitude 0, and on no dateline box). - How likely. Plausible rather than common: the pairing looks little used (no upstream test combines Geo3d with these strategies), and no public report was found for the nine years since 7.1.0.
The test shows that PointVectorStrategy in the Geo3d spatial context misses a point at longitude 175 for Intersects(rect(170, -170, -10, 10)), because Geo3dRectangleShape.getCrossesDateLine() tests maxX > 0 && minX < 0 (spans longitude 0) instead of minX > maxX.
/*
* 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.spatial4j;
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.IndexSearcher;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.apache.lucene.spatial.vector.PointVectorStrategy;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.shape.ShapeFactory;
public class TestGeo3dDatelineBoxQuery extends LuceneTestCase {
public void testPointInsideDatelineBox() throws Exception {
SpatialContext ctx = new Geo3dSpatialContextFactory().newSpatialContext();
ShapeFactory shapes = ctx.getShapeFactory();
PointVectorStrategy strategy = PointVectorStrategy.newInstance(ctx, "pv");
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc = new Document();
for (Field f : strategy.createIndexableFields(shapes.pointXY(175, 0))) {
doc.add(f);
}
w.addDocument(doc);
}
try (DirectoryReader reader = DirectoryReader.open(dir)) {
SpatialArgs args =
new SpatialArgs(SpatialOperation.Intersects, shapes.rect(170, -170, -10, 10));
// expected: 1 hit, the point at longitude 175 is inside the box 170..-170; actual: 0
assertEquals(1, new IndexSearcher(reader).count(strategy.makeQuery(args)));
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-671/, 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.spatial4j.TestGeo3dDatelineBoxQuery
# the module(s) spatial-extras also need their third-party runtime jars
- Expected vs actual
- Expected 1 hit; actual: 0 (
expected:<1> but was:<0>), with-eaand-da.
More files, environment, full output34 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: expected:<1> but was:<0>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:647)
org.junit.Assert.assertEquals(Assert.java:633)
org.apache.lucene.spatial.spatial4j.TestGeo3dDatelineBoxQuery.testPointInsideDatelineBox(TestGeo3dDatelineBoxQuery.java:52)- The class's own
getWidth()treatsminX > maxXas a wrap (170..-170 is 20 degrees wide), and spatial4j'sRectangleImpldefines the flag asminX > maxX. - spatial4j's
Rectanglejavadoc: a geo rectangle "may cross the international date line (-180 longitude) if getCrossesDateLine()". - In the introducing PR,
getWidth()started with the same wrong test and was corrected; the flag was not.
The full argument238 words
- The class contradicts itself.
getWidth()treatsminX > maxXas a wrap, and Geo3d's ownrelate()puts (175, 0) inside 170..-170 and (100, 0) outside -10..10, while the flag says the opposite (independent reproducer, this doc's test, the diagram probe). - Contract. spatial4j 0.8
Rectangleclass javadoc: "In geospatial contexts, it may cross the international date line (-180 longitude) if getCrossesDateLine()";getMinX()andgetMaxX()are the left and right edges; the referenceRectangleImpl.getCrossesDateLine()isminX > maxX. Lucene'sBBoxStrategyjavadoc: "a boolean to mark a dateline cross" (BBoxStrategy.java:63); the bbox package: "a flag to say if it crosses the dateline" (package-info.java:21). A box from -10 to 10 does not touch -180. - History. In the PR commit that first added the class (lucene-solr#246,
ea31e5ce0f, 2017-09-07; the PR's first commit,3ba3dad6b7, adds onlyGeo3dAreaShape)getWidth()andgetCrossesDateLine()shared the sameminX < 0 && maxX > 0test; commitcfbac2fb00(2017-09-12) rewrotegetWidth()to the wrap convention and left the flag unchanged. No review comment discusses it. Before 7.1.0,Geo3dShape.getBoundingBox()returned a spatial4jRectangleImplwith the right flag; 7.0.0 and 7.0.1 pass the version-portable test. - Tests. No upstream test calls
getCrossesDateLine()on a Geo3d rectangle or pairs a Geo3d context withPointVectorStrategyorBBoxStrategy; the Geo3d tests use RPT,SerializedDVStrategy(TestGeo3dRpt) orrelate(), never these two strategies. No test, CHANGES or MIGRATE entry accepts the "spans longitude 0" meaning.
The devil's-advocate check162 words
The strongest case for "intended" (an intended-behaviour checker): every class involved is @lucene.experimental; the Rectangle method javadoc gives no formula ("Only meaningful for geospatial contexts."); the Geo3d bridge was built for prefix-tree strategies, so Geo3d with BBoxStrategy or PointVectorStrategy may never have been a supported pairing; and the line has not changed since 2017 without anyone reporting it.
Why it fails: experimental status warns about API changes, not wrong answers; the class javadoc and Lucene's own BBoxStrategy docs define the meaning, and the class's own getWidth() follows it; neither strategy restricts its SpatialContext, and in the PR the reviewer said the Geo3d rectangle "could be used for a variety of purposes, notably a query for a rectangular map area"; age is not intent, and the PR history shows the sibling method was corrected and this one missed. Verdict: not intended, a defect (the adjudication and independent checks; the cross-model reviewer rated the intended-behaviour argument weak under both of its lenses).
Recommended A: use spatial4j's definition, minX > maxX
Return getMinX() > getMaxX(), spatial4j's own definition: a one-line change in Geo3dRectangleShape.getCrossesDateLine(). The new test, every earlier repro and the whole spatial-extras module with its checks pass. BBoxStrategy indexes built with the bug store the wrong flag and need a reindex.
@Override
public boolean getCrossesDateLine() {
return getMinX() > getMaxX();
}All options and trade-offs3 options ยท 502 words
One option was built and validated in a scratch worktree at a9779ef3c193 with gradle (JDK 25) and with the standalone runner against every repro (patched class shadowed). The other two were weighed and not built. Any fix needs a regression test (this doc's test is one) and a reindex note for BBoxStrategy users.
Change line 154 of Geo3dRectangleShape.java. It is the definition of spatial4j's RectangleImpl and the convention the class already uses in getWidth() and its factory. It fixes the query side and the index side of both strategies, and every other caller of the flag gets the Rectangle contract.
@Override
public boolean getCrossesDateLine() {
return getMinX() > getMaxX();
}Trade-offs
- Smallest change, at the defect; the same change as the finder's fix.
- By code reading, the full-world box -180..180 (also the bounding box of any shape without a longitude bound) now reports
falseinstead oftrue, the same as spatial4j'sRectangleImpl; the module's tests pass. - Documents indexed by
BBoxStrategybefore the fix keep their oldxdlterm (Ton boxes that span longitude 0,Fon dateline boxes) until reindexed: worth a CHANGES or MIGRATE line.
What the checks validated
- Gradle, scratch worktree at
a9779ef3c193, seed DEADBEEF, this doc's test added: it fails 6 of 6 on stock main (assertions on and off) and passes 6 of 6 with the fix. ./gradlew :lucene:spatial-extras:checkwith the fix passes: 27 suites, 123 tests (the module's 117 plus the 6 new), 5 skipped, 0 failures, and the format, ecjLint and forbiddenApis checks. The suites includeTestBBoxStrategy,TestPointVectorStrategy,TestGeo3d,TestGeo3dRpt, both Geo3d RectRelation tests andTestHeatmapFacetCounter.TestBBoxStrategy,TestPointVectorStrategyandTestGeo3d*10 times with a random root seed and assertions off: 430 tests, 0 failures.- Standalone, fix shadowed: this doc's test 6/6 (assertions on and off); earlier, by the hunt: the finder's 5/5, the independent reproducer's 5/5, the cross-model reviewer's 7/7 and 10/10, the devil's advocate's 8/8, the history test 5/5, the security probe with 0 wrong results (it exits 1 only on its own checks that the bug is still active, as expected with the fix), and the end-to-end differential with only 4 unrelated +-180 edge cases left.
PointVectorStrategy.makeWithin and BBoxStrategy (index and query side) would stop calling getCrossesDateLine().
Trade-offs
Touches five call sites in the two strategies (PointVectorStrategy.java:212; BBoxStrategy.java:206, :299, :382, :542) and leaves the Rectangle contract broken for every other caller (HeatmapFacetCounter, BBoxOverlapRatioValueSource, user code). Not recommended.
Let Geo3dShape.getBoundingBox() return a spatial4j RectangleImpl again, as before 7.1.0.
Trade-offs
Not sufficient: ctx.getShapeFactory().rect(...) and WKT ENVELOPE still build a Geo3dRectangleShape with the wrong flag, so dateline query boxes stay broken. The 7.0 bounding box was also buffered by 0.05 degrees (ROUNDOFF_ADJUSTMENT in 7.0.1's Geo3dShape), which C would have to decide on.
Applications that use a Geo3d-context BBoxStrategy or PointVectorStrategy clause as a geographic allow or deny rule (Solr's reference guide lists the Geo3D context for BBoxField; not run) get wrong decisions at longitude 0 and the dateline, but the wrong matches never crossed other filter clauses or returned deleted documents in the executed probe.
Model-based differential testing of the spatial4j-to-spatial3d bridge (not formal verification). The line was first suspected while writing a model rule: a shape's bounding box, read through the Rectangle contract, must contain the shape. A harness then found the flag disagreeing with minX > maxX on 3,693 bounding boxes, and an end-to-end harness found wrong strategy hits.
The method in full210 words
Method: semi-formal: a hypothesis formed by code reading while writing a model rule, then witnessed by model-based differential testing, in a hunt unit on the spatial4j bridge to spatial3d.
- While writing the bridge model's rule G2 (a Geo3d shape's bounding box, read through the
Rectanglecontract, must enclose every sampled member of the shape), the finder suspected the flag from the code. - The harness
Geo3dBridgeDiffthen checked 10,000 random and boundary shapes (dateline, poles, degenerate and near-hemisphere boxes, tiny circles; SPHERE and WGS84; seed 5EED220):getCrossesDateLine()disagreed withminX > maxXon 3,693 bounding boxes, and 1,091 bounding boxes failed to enclose their own members when read through the flag. - End to end,
Geo3dStrategyDiff(300 documents and 150 queries per planet model, the spatial4j context as the oracle):PointVectorStrategymissed hits on 42 of 300 queries;BBoxStrategyIntersects missed hits on 265 of 300 and returned false hits on 220 of 300. With the one-line fix only 4 of its 900BBoxStrategyqueries (all Within or Contains, with an edge at -180) still differed, an unrelated representation issue. - A public-API JUnit on the stock main jar confirmed it; the devil's advocate later added the
PointVectorStrategyfalse positive for boxes around longitude 0, which the end-to-end harness had not counted.
No prior report found (2026-10-27): two runs of the prior-report tool (51 and 89 queries; the second run's saved report lists 308 distinct hits, all triaged), 28 manual GitHub searches, 3 code searches, 14 JIRA queries, the Lucene and Solr lists and the web. Closest: #9000 (LUCENE-7951), the change that introduced it, whose review never mentions the flag.
Searches4 searches
None of the hits is this bug. The only earlier mention is the hunt's own LFV-611 side observation. Limits: keyword search only; private security reports and unindexed chat cannot be searched.
| Where | When | How | Found |
|---|---|---|---|
| the prior-report tool: GitHub issues, PRs and commits (apache/lucene), downstream repositories, the lists, JIRA and the web | 2026-10-27 | two runs: 51 and 89 queries, 6 web queries in each (identifiers such as Geo3dRectangleShape, getCrossesDateLine, Geo3dSpatialContextFactory, BBoxStrategy, PointVectorStrategy; symptoms such as Geo3d dateline, BBox dateline); the second run's saved report lists 308 distinct hits, 37 of them from the web (its run log, written before the web results were merged, counts 271); every hit created after 2017-09-01 triaged | no report; related: #9000, #9568, #9134, #9038, #6776, #5247 |
| GitHub, manual and code search | 2026-10-27 | 28 phrasings across apache/lucene, apache/solr, locationtech/spatial4j, Elasticsearch and OpenSearch; 3 code searches | nothing matching; 33 vendored copies of the file, none with a corrected predicate |
| Open PRs | 2026-10-27 | the 4 open module:spatial PRs (#16847, #14996, #14599, #13948) | none touches the spatial4j, vector or bbox packages |
| JIRA and mailing lists | 2026-10-27 | 14 JQL queries (LUCENE, SOLR), 12 issues read; dev@ and java-user@ lucene, users@, dev@ and solr-user@ solr | SOLR-11382 (added the Geo3D alias, no dateline discussion); 2019 solr-user threads with other causes |
Introduced in 7.1.0 by LUCENE-7951 (035523ba7a86 on main, 85491401d54c on branch_7x, 2017-09-20). In its PR, getWidth() was first written with the same test and later corrected; the flag was not. 7.0.0 and 7.0.1 pass the history test; 7.1.0 to 10.5.2 and main fail. Never fixed.
Versions tested5 rows
Introduced by LUCENE-7951 "Spatial4j implementations using Geo3d" (#9000): code from lucene-solr#246 by @iverase (closed without a GitHub merge), committed by @dsmiley on 2017-09-20 as 035523ba7a86 on main (first released in 8.0.0) and the identical cherry-pick 85491401d54c on branch_7x, which shipped in 7.1.0. git log -S finds the line added only by these two commits (on main's history only 035523ba7a86; over all refs the other two hits, 527c6f350c5 of 2018 and aad44bc6e36 of 2021, only move the file or delete the whole old branch_8x tree, and neither is in main's history); the only later commit touching the method on main is the LUCENE-9570 reformat 2cbf261032d (2021). All 63 release tags from 7.1.0 to 10.5.2 in the local clone carry the line; its 8.x tags stop at 8.8.1, so 8.8.2 and 8.9.0 to 8.11.x are inferred (8.11.4 was run from its release jar). Before 7.1.0, Geo3dShape.getBoundingBox() returned a spatial4j RectangleImpl, so for users who wrapped spatial3d shapes in Geo3dShape this is a regression. Upstream main, branch_10x and branch_10_6 still have the line.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | this doc's test fails 6 of 6, assertions on and off (gradle and standalone) |
| 10.5.2, 9.12.3 | affected | this doc's test fails 6 of 6; the finder's and the history test 5 of 5 |
| 8.11.4, 8.0.0, 7.7.3, 7.1.0 | affected | the finder's test and the history test fail 5 of 5 |
| 7.0.1, 7.0.0 | not affected | the history test passes 5 of 5 (the bounding box is a spatial4j RectangleImpl); the Geo3d spatial context does not exist yet |
| fix on main | not affected | every JUnit repro passes and the security probe finds no wrong result; the spatial-extras module with its checks passes |
Reproduced by the finder, an independent reproducer, two executed reviews by another model family (OpenAI GPT-5.6), a devil's advocate, the adjudication, the follow-up and a credibility audit, each with its own runs. All rated it real; the devil's advocate added the false positives for boxes around longitude 0.
Checks and verdicts248 words
Each check ran its own tests on main a9779ef3c193 (the cross-model reviews on ea57360912ed, whose spatial-extras and spatial3d code is the same).
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Bridge model and end-to-end differential; a 5-test JUnit fails 5 of 5 with assertions on and off and passes with the fix. |
| Independent reproducer | real | Own test with other coordinates and spatial4j controls fails 5 of 5 on main, 10.5.2 and 9.12.3; the Geo3d shapes themselves behave as dateline shapes while the flag disagrees. |
| refute review (OpenAI GPT-5.6, executed) | real, high | Own tests: 7 run, 3 failures with assertions on and off; 4 controls pass; 7 of 7 pass with the fix. |
| intended review (OpenAI GPT-5.6, executed) | real, high | Intended-behaviour argument weak; own test: 10 run, 5 failures covering both flag directions; 10 of 10 pass with the fix. |
| Devil's advocate | real, high | Built the strongest intended case and showed why it fails; added the PointVectorStrategy false positive, the WKT path and the PR history (getWidth corrected, flag not). |
| adjudication | confirmed, high | Re-ran the finder's and the independent reproduction's tests (5 of 5 fail) and the independent reproduction's with the fix (5 of 5 pass). |
| Follow-up | regression in 7.1.0 | Release boundary with a version-portable test; security probe; prior-report search. |
| credibility audit | keep | Re-ran the finder's test: 15 of 15 fail with assertions on and 15 of 15 off; the fix passes 5 of 5. |
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.
- PNG 461 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,474 words
- Registry title
- Geo3dRectangleShape.getCrossesDateLine() tests whether a box spans longitude 0 instead of whether it crosses the dateline, so PointVectorStrategy and BBoxStrategy return wrong hits for Geo3d boxes and circles at the dateline or the prime meridian
- Area
- spatial-extras / spatial4j bridge to spatial3d (Geo3dRectangleShape.getCrossesDateLine), read by PointVectorStrategy and BBoxStrategy
- Priority
- Medium, rank 201: PointVectorStrategy and BBoxStrategy silently return wrong hits for ordinary boxes that cross the dateline or span longitude 0, and BBoxStrategy writes a wrong flag that needs a reindex, but only in the opt-in Geo3d context with two little-used strategies.
- Impact class
- silent wrong results
- Impact note
- Applications that use PointVectorStrategy or BBoxStrategy with the opt-in, @lucene.experimental Geo3d spatial context (Geo3dSpatialContextFactory; Solr exposes it as spatialContextFactory="Geo3D", read, not run), or with spatial3d shapes wrapped in Geo3dShape under the default geo context, get wrong hits with no error for every query or document box that crosses the dateline or spans longitude 0: PointVectorStrategy misses points in dateline boxes and in circles whose bounding box crosses the dateline, and matches points at any longitude within the latitude range of a box that spans longitude 0; BBoxStrategy stores the wrong xdl flag at index time, so indexed dateline boxes are missed and indexed boxes spanning longitude 0 match queries on the other side of the planet; it is deterministic with -ea and -da, needs no scale and is in every release since 7.1.0; RecursivePrefixTreeStrategy, the default spatial4j shapes and Lucene's core geo types are not affected, the index is not corrupted (CheckIndex clean), but BBoxStrategy indexes built with the bug keep the wrong flag until they are reindexed.
- Summary
- You use the Geo3d spatial context (Geo3dSpatialContextFactory) with PointVectorStrategy or BBoxStrategy and query or index a box that crosses the dateline, such as rect(170, -170, -10, 10), or one that spans longitude 0, such as rect(-10, 10, -10, 10). You expect getCrossesDateLine() to follow the spatial4j Rectangle contract (true exactly when the box crosses the -180/180 meridian, that is minX > maxX, as spatial4j's RectangleImpl and this class's own getWidth() assume), so a point at longitude 175 is found inside 170..-170 and an indexed box -10..10 does not match a query box at 100..110. Instead Geo3dRectangleShape.getCrossesDateLine() returns getMaxX() > 0 && getMinX() < 0 ('spans longitude 0'), which is false for 170..-170 and true for -10..10: PointVectorStrategy searches the empty range 170..-170 and returns 0 hits (also for circle(180, 0, 5)), and BBoxStrategy stores xdl=F for the dateline box and misses it (0 hits instead of 1), but stores xdl=T for -10..10, reads it as [-10, 180] plus [-180, 10] and matches it against 100..110 (1 hit instead of 0).
- Introduced
- 035523ba7a86773500c053129f65c9a6dc4fd384 (master) and its branch_7x cherry-pick 85491401d54c59e1593a5c9f13dca7bfcdda31a2 (the commit that shipped in 7.1.0): LUCENE-7951 'Spatial4j implementations using Geo3d' (migrated as apache/lucene#9000), from PR apache/lucene-solr#246 by GitHub iverase, committed directly by GitHub dsmiley (the PR was closed without a GitHub merge). The new class Geo3dRectangleShape had 'return (getMaxX() > 0 && getMinX() < 0);' from the start [read: git log -S over the full history finds only these two commits; git blame -w -M -C and git log -L agree; the only later commit touching the method is the LUCENE-9570 reformat 2cbf261032d]. In the PR's first commit ea31e5ce0f getWidth() used the same test; cfbac2fb00 rewrote getWidth() to the wrap convention and left getCrossesDateLine() unchanged [read: GitHub API GETs]. A regression for users of Geo3dShape: before 7.1.0 its getBoundingBox() returned a spatial4j RectangleImpl with the correct flag [exec: the bisection test passes on 7.0.0 and 7.0.1 and fails on 7.1.0]. (2017-09-20 (commit date 23:09:46 -0400, i.e. 2017-09-21T03:09:46Z))
- First affected version
- 7.1.0 [exec: the primary repro and the version-portable bisection test both fail 5 of 5]; 7.0.0 and 7.0.1 are not affected [exec: the bisection test passes 5 of 5; the Geo3d spatial4j factory classes do not exist there, so the primary repro does not compile on 7.0.1]
- Affected versions
- Every release from 7.1.0 through 10.6.0 (10.6.0 by reading) and main a9779ef3c193; not fixed upstream. [exec, JDK 25, SHA-1-checked release jars] the finder's JUnit (run by the finder and the follow-up; the 7.x and 8.x runs use a source variant that differs only in the LuceneTestCase import) fails 5 of 5 on 7.1.0, 7.7.3, 8.0.0, 8.11.4, 9.12.3, 10.5.2 and main (-ea and -da), and the bisection test fails 5 of 5 on the same releases and main and passes on 7.0.0 and 7.0.1 (follow-up); the independent reproduction's JUnit fails 5 of 5 on main (-ea and -da), 10.5.2 and 9.12.3; the intended-behaviour check's JUnit fails 5 of 8 on main (-ea and -da) and 10.5.2. [read] the line is unchanged since 035523ba7a8 except for the 2021 reformat; the 8.9.0-8.10.x tags are not in the local clone (inferred affected from 8.8.1 and the executed 8.11.4); upstream main, branch_10x and branch_10_6 still have the line (read 2026-10-27).
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: code reading
- Upstream and prior reports
- none found (searched 2026-10-27, read-only; the prior-report record, verdict new): two scripted prior-report searches (45 and 89 queries over GitHub issues and PRs, commits, downstream repositories, the dev, java-user, issues, commits and solr-user lists, JIRA and the web; the second returned 308 distinct hits and every hit created after 2017-09-01 was triaged), 28 manual GitHub searches (apache/lucene, apache/solr, locationtech/spatial4j, Elasticsearch, OpenSearch), 3 GitHub code searches (33 vendored copies of the file, none with a corrected predicate), 14 JIRA queries over LUCENE and SOLR with 12 issues read, list searches over the Lucene and Solr user and dev lists, 8 web queries and the file's full git history. Nothing reports or discusses Geo3dRectangleShape.getCrossesDateLine or wrong PointVectorStrategy/BBoxStrategy results with Geo3d shapes. Not fixed upstream: upstream main, branch_10x and branch_10_6 still have the line (read 2026-10-27), the file's only commits are 035523ba7a86 and the 2021 reformat 2cbf261032dc, and none of the 4 open module:spatial PRs (#16847, #14996, #14599, #13948) touches the spatial4j, vector or bbox packages. Related items, not reports of this bug: #9000 / LUCENE-7951 and apache/lucene-solr#246 (the introducing issue and PR; no comment on the flag), #9568 / LUCENE-8522 (an NPE in Geo3dShape.getBoundingBox), #9134 / LUCENE-8086, #9038 / LUCENE-7990 and LUCENE-8054 (Geo3d geometry fixes), SOLR-11382 (added Solr's Geo3D context alias), #6776 / LUCENE-5714 and #5247 / LUCENE-4175 (BBoxStrategy dateline work with the spatial4j context, before the Geo3d bridge existed).
- Lucene commit tested
- main
a9779ef3c193(commit of 2026-10-26; tested 2026-10-27) with gradle in a scratch worktree and with the standalone JUnit runner, assertions on and off; the 10.5.2 (assertions on and off) and 9.12.3 (on) release jars. The hunt, verifiers, judge and audit ran on maina9779ef3c193(the cross-model reviews onea57360912ed, whose spatial-extras and spatial3d sources are the same); the follow-up ran its tests on 7.0.0, 7.0.1, 7.1.0, 7.7.3, 8.0.0, 8.11.4, 9.12.3, 10.5.2 and main. The fix was validated with gradle (the new test, the closest suites 10 times, the whole spatial-extras module with its checks) and with the standalone runner against every repro.
- Finding record
lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dRectangleShape.java:35 (at a9779ef3c193) (@lucene.experimental), :119-125 (getWidth adds 360 when maxX - minX < 0), :152-155 (getCrossesDateLine returns getMaxX() > 0 && getMinX() < 0)lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShapeFactory.java:163-173 (at a9779ef3c193) (rect(minX, maxX, ...) keeps minX > maxX for a dateline box) and lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShape.java:107-118 (getBoundingBox returns a Geo3dRectangleShape for every Geo3d shape); lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dSpatialContextFactory.java:28 (@lucene.experimental)lucene/spatial-extras/src/java/org/apache/lucene/spatial/vector/PointVectorStrategy.java:80 (at a9779ef3c193) (@lucene.experimental), :209-222 (makeWithin: two longitude ranges only when getCrossesDateLine(), else one range [minX, maxX])lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java:54-71 (at a9779ef3c193) (javadoc 'a boolean to mark a dateline cross', @lucene.experimental), :206 (xdl field written from the flag), :299, :382, :542 (query branches on the flag); other readers of the flag, not shown to misbehave: lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/HeatmapFacetCounter.java:257 and lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java:163-184
- The finding in 30 seconds
lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dRectangleShape.java:35 (at a9779ef3c193) (@lucene.experimental), :118-125 (getWidth wraps), :152-155 (getCrossesDateLine)lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShapeFactory.java:163-173; (at a9779ef3c193) lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShape.java:107-118; lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dSpatialContextFactory.java:28 (@lucene.experimental)lucene/spatial-extras/src/java/org/apache/lucene/spatial/vector/PointVectorStrategy.java:188-222 (at a9779ef3c193)lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java:63, (at a9779ef3c193) 205-207, 299, 382, 473-496, 542
- Who is affected
grep -rn getCrossesDateLine over a Lucene main checkout lucene/*/src/java: only spatial-extras callers
- Why it looks unintended
lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java:63; (at a9779ef3c193) lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/package-info.java:21
- Suggested fix
lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dRectangleShape.java:64-83 (at a9779ef3c193) (bounds without a longitude bound become -180..180)
- TestGeo3dDatelineBoxQuery.java (JUnit test):
repro/LFV-671/src/org/apache/lucene/spatial/spatial4j/TestGeo3dDatelineBoxQuery.java(56 lines)
A step-by-step picture made in parallel with this doc. Every flag, query and hit on screen is parsed from its own executed probe (main a9779ef3c193 with assertions on and off, and with the fix shadowed), with the same points and boxes as this doc.
assets/LFV-671/bug.png(461 KB)
