LatLonShape and XYShape index lines and polygons as segments and triangles. An INTERSECTS polygon query asks, for each of them, whether it touches the query polygon; DISJOINT returns the documents where none does. The query polygon may have holes.
Polygon2D.intersectsLine that never test the hole edges, and the fix. Select the still to open it at full size. Generated with AI, checked against the code.- Query with a polygon that has holes, like a region with two lakes cut out: the square [0,10] x [0,10] with lakes [1,3] x [4,6] and [7,9] x [4,6]. The land strip between the lakes, for example the point (5,5), belongs to the polygon.
- Index three shapes that all touch (5,5): the line (2,5)-(8,5) from one lake to the other, the box [2,8] x [4.5,5.5] with its corners in the lakes, and the point (5,5). You expect INTERSECTS to return 3 documents and DISJOINT 0.
- You get INTERSECTS 1 (only the point) and DISJOINT 2 (the line and the box), with
XYShapeandLatLonShapealike, default settings, and no error. - Why: for each indexed segment or triangle,
Polygon2Dfirst asks "is a vertex on land, or does an edge cross the outer ring?". A vertex in a lake does not count as on land, and that first test does not look at the lake shores, so the line fails it and is reported as not intersecting. - 8.5.2 returns 3 and 0: LUCENE-9273, a speed-up in 8.6.0, dropped its hole-first check. A few-line fix restores 3 and 0, and all 8,894
lucene/coretests pass with it.
Anyone running LatLonShape or XYShape polygon or geometry queries with INTERSECTS or DISJOINT on 8.6.0 to 10.6.0 (10.6.0 by reading) or main, default settings. The query polygon needs two or more holes, or one non-convex hole, and a missed document has every vertex in holes. Point documents are not affected. Rare, but silent.
Mechanism, terms and numbers671 words
LatLonShape and XYShape polygon queries test each indexed segment and triangle with Polygon2D.intersectsLine / intersectsTriangle; DISJOINT is the negation of the same test. Both methods first ask whether a vertex lies inside the polygon, with the hole-aware contains() (a vertex in a hole counts as outside), or whether an edge crosses the outer ring; only then do they consult the holes, and only to exclude a shape that lies inside one hole. That first test never looks at hole edges. So a segment or triangle whose vertices all lie in holes, and which does not reach the outer ring, is reported as not intersecting even when it crosses polygon area between the holes. With the square [0,10] x [0,10] minus holes [1,3] x [4,6] and [7,9] x [4,6], the line (2,5)-(8,5) and the box [2,8] x [4.5,5.5] are missed by INTERSECTS and returned by DISJOINT, although contains(5,5) is true. It needs a query polygon with two or more holes (or one non-convex hole); point documents are not affected. LUCENE-9273 (8.6.0, 2020), a performance refactoring, dropped the hole-first check that 8.5.2 had; every release from 8.6.0 to 10.6.0 (10.6.0 by reading) and main is affected. Testing the vertices against the outer ring only fixes it.
- Query polygon with holes: an outer ring (clockwise) and holes (counter-clockwise) inside it, as the
Polygonjavadoc describes (XYPolygontakes holes the same way). Points in a hole are not part of the polygon. Polygon2D: the query-side geometry built from such a polygon.treeholds the outer ring's edges; the holes are a separate component (Polygon2D.java:45,:369-386).- Leaf predicate: for every indexed segment and triangle, INTERSECTS calls
intersectsLine/intersectsTriangle(LatLonShapeQuery.java:107,:117;XYShapeQuery.java:89,:99). A document matches if any of its segments or triangles does. DISJOINT is the negation (SpatialQuery.java:129-130).
contains(x, y)is "inside the outer ring and not in a hole" (Polygon2D.java:111-117, the hole test at:114).intersectsLine(:155-174) returns true only ifcontains(a) || contains(b) || tree.crossesLine(...)(:168-170); then it excludes a segment that one hole contains (holes.containsLine(...) == false,:171).intersectsTriangle(:176-201) does the same with three vertices, the outer ring's first vertex andtree.crossesTriangle(:191-198).treeis the outer ring only, so this first test never looks at hole edges. For the line (2,5)-(8,5):contains(2,5)is false (hole A),contains(8,5)is false (hole B), and the line does not reach the outer ring, so the method returns false at:173. The hole check that would accept it is never reached.relate()(:119-153), used for BKD inner nodes, checks the holes first (:127-135), so nothing is pruned early; the wrong answer comes from the leaf predicate, which is final.- Multi-polygon and mixed geometry queries reach the same method through
ComponentTree(ComponentTree.java:112-150,:153-195). Point documents usecontains()and are correct.
Query: the square [0,10] x [0,10] with holes A = [1,3] x [4,6] and B = [7,9] x [4,6]; contains(5,5) is true.
| indexed document | touches the land at (5,5)? | INTERSECTS expected / actual | DISJOINT expected / actual |
|---|---|---|---|
| line (2,5)-(8,5), ends in holes A and B | yes | match / missed | no match / match |
| box [2,8] x [4.5,5.5], corners in the holes | yes | match / missed | no match / match |
| point (5,5) (control) | yes | match / match | no match / no match |
Counts: INTERSECTS 1 instead of 3, DISJOINT 2 instead of 0, for XYShape and LatLonShape (degrees), with assertions on and off. The same counts with the holed polygon inside a two-polygon query or a polygon-plus-point geometry query (run with LatLonShape; XYShape goes through the same ComponentTree code, by reading). WITHIN on the same data is correct (1, the point). Other shapes that trigger it: a line between the two arms of one U-shaped hole, through the land in the notch; a triangle with one vertex in each of three holes.
Exposure and likelihood334 words
- Who is exposed. Applications that query
LatLonShapeorXYShapefields with a polygon that has holes, throughnewPolygonQueryornewGeometryQuery, with INTERSECTS or DISJOINT, on every release since 8.6.0. Default codec,IndexWriterConfigand searcher; nothing has to be enabled. Downstream products that build their shape queries on these classes with holed polygons would inherit it; no downstream product was tested. - What it takes. (1) A query polygon with two or more holes, or one non-convex hole: one convex hole cannot trigger it, because a segment or triangle with all its vertices in a convex hole lies inside it. (2) An indexed line or polygon whose every segment or triangle has all its vertices in holes, does not reach the outer ring, and crosses land between the holes. A single vertex on land, or one edge that crosses the outer ring, makes the document match. Examples: a straight two-point line between two lakes cut out of a region; a coarse rectangle whose corners fall in two lakes.
- What the damage looks like. Silent: no exception and no log line; INTERSECTS drops the document and DISJOINT returns it. The error is geometric, not a precision effect: every vertex of the repro is at least 0.5 units (or degrees) from every ring edge, more than a million times the encoding quantum (4.19e-8 degrees of latitude, 8.38e-8 of longitude). By reading, index size does not matter: a BKD cell holding such a shape also covers part of a hole, so
relate()reports CROSSES and the leaf predicate decides. - How likely. Rare: most indexed shapes have a vertex on land or cross the outer ring, and no report was found since 2020.
- Not affected. Point documents;
LatLonPointandXYPointpolygon queries; query polygons without holes; WITHIN on this data. CONTAINS has its own, independent hole bug in the same class (Polygon2D.withinTriangle, since 8.4.0). Not run: shape doc values. Their only public query is a box query (newSlowDocValuesBoxQuery), althoughShapeDocValuescalls the same two predicates.
The test shows that an XYShape INTERSECTS query with a polygon that has two holes misses a line whose end points lie in the holes but which crosses the polygon between them (Polygon2D.intersectsLine never tests the hole edges).
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.document;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.XYLine;
import org.apache.lucene.geo.XYPolygon;
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.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestXYShapeLineBetweenHoles extends LuceneTestCase {
public void testLineFromHoleToHoleIntersects() throws Exception {
XYPolygon holeA = new XYPolygon(new float[] {1, 3, 3, 1, 1}, new float[] {4, 4, 6, 6, 4});
XYPolygon holeB = new XYPolygon(new float[] {7, 9, 9, 7, 7}, new float[] {4, 4, 6, 6, 4});
XYPolygon square =
new XYPolygon(new float[] {0, 0, 10, 10, 0}, new float[] {0, 10, 10, 0, 0}, holeA, holeB);
try (Directory dir = new ByteBuffersDirectory();
IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc = new Document();
for (Field f :
XYShape.createIndexableFields("f", new XYLine(new float[] {2, 8}, new float[] {5, 5}))) {
doc.add(f);
}
w.addDocument(doc);
try (DirectoryReader r = DirectoryReader.open(w)) {
// expected 1: the line crosses the polygon between the holes at (5,5); actual 0
assertEquals(
1,
new IndexSearcher(r)
.count(XYShape.newPolygonQuery("f", QueryRelation.INTERSECTS, square)));
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-698/, 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: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.document.TestXYShapeLineBetweenHoles
- Expected vs actual
- Expected count 1 for the line (2,5)-(8,5) against the square [0,10]^2 minus [1,3]x[4,6] and [7,9]x[4,6]; 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.document.TestXYShapeLineBetweenHoles.testLineFromHoleToHoleIntersects(TestXYShapeLineBetweenHoles.java:47)newPolygonQuerypromises "all indexed geo shapes that intersect a provided polygon"; its javadoc excludes only dateline crossing, andPolygonsupports holes.Polygon2Dcontradicts itself:contains(5,5)is true, yet the segment through (5,5) does not intersect.- 8.5.2 checked the holes first and answers 3 and 0; LUCENE-9273, a speed-up, never discussed holes.
The full argument277 words
- Contract.
Component2D.java:49and:60: "return true if this component2D intersects the provided line" (triangle).LatLonShape.java:294-297: "create a query to find all indexed geo shapes that intersect a provided polygon (or array of polygons) note: does not support dateline crossing";XYShape.java:227-230says the same for cartesian shapes.Polygon.java:31-32: "Coordinates must be in clockwise order, except for holes. Holes must be in counter-clockwise order."Polygon.java:33-35adds closed rings, no self-crossing and decimal degrees. The repro follows all of these rules: closed rings, clockwise shells, counter-clockwise holes strictly inside and disjoint. - Self-contradiction. The same object says (5,5) is inside (
contains) and that the segment (2,5)-(8,5), which runs through (5,5), does not intersect. - History. Before 8.6.0,
Polygon2D.relateTrianglebegan with "// check any holes": a shape crossing a hole edge counted as crossing the polygon (parent37ad0e552dc,Polygon2D.java:145-152; the step dates from LUCENE-8435, 7.5.0). LUCENE-9273 (f914e08b364, 2020-04-20, "Speed up geometry queries by specialising Component2D spatial operations") replaced it with the specialised methods and dropped the step. CHANGES lists it under Optimizations; the issue, the PR and its single review never mention holes. Commit-level builds: both parents pass, both commits fail. - Tests. The random shape tests take their expected answer from the same predicates (
BaseSpatialTestCase.java:747-797callsintersectsLine/intersectsTriangle), and the random polygon generators build no query polygons with holes (TestPolygon2D.java:364: "currently we don't generate these").TestLatLonShape's hole tests query indexed holed polygons with a box. No test asserts the current answer for a hole-to-hole shape, and the fix breaks no test. - Todo left open. LUCENE-8435 (gh#9481), which added these queries, lists "add testing for arbitrary polygons with holes".
The devil's-advocate check163 words
The strongest "intended" case: Component2D is @lucene.internal (Component2D.java:27) with one-line javadocs; Polygon is @lucene.experimental; LatLonShape warns that vertex values lose precision (LatLonShape.java:72); TestPolygon2D says relate(box) "can conservatively return CELL_CROSSES_QUERY" (TestPolygon2D.java:50); the random shape tests pass; and the trigger is rare.
Why it fails: the public newPolygonQuery states the contract and lists only the dateline as a limitation; @lucene.experimental is about API stability, not wrong answers; the precision warning covers about 1e-7 degrees, while every repro vertex is at least half a degree from any ring edge; "conservative" covers the inner-node relate(box) pre-filter, while the leaf predicate is final and DISJOINT is its negation; the passing tests use the code under test as their oracle and never build holed query polygons; rarity bounds the impact but does not make the answer intended; and 8.5.2 answered correctly. Verdict: a defect (adjudication: confirmed, high; the reproducer, the devil's advocate and the intended-lens review all rated the intended case weak).
Recommended Test the vertices against the outer ring only
In Polygon2D.intersectsLine and intersectsTriangle, test the vertices against the outer ring only (bounding box and tree.contains) instead of the hole-aware contains(); the existing "inside one hole" check then does the hole work. The repro, the 50 geo and shape suites and all 8,894 lucene/core tests pass.
/** true if the point is inside or on the outer ring, ignoring holes */
private boolean shellContains(double x, double y) {
return Component2D.containsPoint(x, y, minX, maxX, minY, maxY) && tree.contains(x, y);
}
...
// test against the outer ring only: a segment whose end points lie in holes can still cross
// the polygon between them; the holes are excluded by the containsLine check below
if (shellContains(aX, aY)
|| shellContains(bX, bY)
|| tree.crossesLine(minX, maxX, minY, maxY, aX, aY, bX, bY, true)) {
return holes == null || holes.containsLine(minX, maxX, minY, maxY, aX, aY, bX, bY) == false;
}
// intersectsTriangle: the same three shellContains callsAll options and trade-offs4 options ยท 818 words
Both options give the same answers on every test and probe here; by reading, their whole pre-tests accept the same shapes whenever the holes lie inside the outer ring, so they differ only in cost. Near a single hole's edge nothing changes: of nine component-level cases, seven of them around hole A, only the two hole-to-hole cases flip, from false to true, with either option (executed). One case to decide first: holes may touch at one vertex (Tessellator.java:43-48). With holes [2,4] x [2,4] and [4,6] x [4,6], the line (3,3)-(5,5), which passes from one hole into the other through the shared corner only, is reported as intersecting by both options and by 8.5.2, but not by stock 8.6.0 and later, and contains(4,4) is false (executed, component level and end to end). Keep 8.5.2's answer, or add an exclusion that knows about the union of holes (not written), and add a test for this case either way. Only query answers change, so no reindex is needed. Add the component tests to TestPolygon2D, and consider letting the random shape tests build query polygons with holes: their current oracle calls the code under test. The CONTAINS sibling (Polygon2D.withinTriangle) needs its own change in the same file.
A private shellContains(x, y) (bounding box and tree.contains) replaces contains() in the pre-tests of intersectsLine and intersectsTriangle; the existing holes.containsLine / containsTriangle check removes shapes that lie inside one hole. When the holes are strictly separated, a segment or triangle that lies wholly in holes lies in one hole; for holes that touch at a vertex, see above. Without holes, shellContains equals contains, so such polygons behave exactly as before. +13/-5 in Polygon2D.java.
/** true if the point is inside or on the outer ring, ignoring holes */
private boolean shellContains(double x, double y) {
return Component2D.containsPoint(x, y, minX, maxX, minY, maxY) && tree.contains(x, y);
}
...
// test against the outer ring only: a segment whose end points lie in holes can still cross
// the polygon between them; the holes are excluded by the containsLine check below
if (shellContains(aX, aY)
|| shellContains(bX, bY)
|| tree.crossesLine(minX, maxX, minY, maxY, aX, aY, bX, bY, true)) {
return holes == null || holes.containsLine(minX, maxX, minY, maxY, aX, aY, bX, bY) == false;
}
// intersectsTriangle: the same three shellContains callsTrade-offs
- The smallest change. By reading it is slightly cheaper than today for vertices on land (it skips the per-vertex hole lookup in
contains()); no benchmark was run. - At a single hole the boundary behaviour is unchanged: a segment or triangle inside a hole that touches or runs along the hole's edge is still excluded, as on stock (executed). A segment through the shared corner of two touching holes now intersects (see above).
What the checks validated
- Scratch worktree of
a9779ef3c193, gradle, JDK 25: the new tests fail 4 of 5 on stock (assertions on and off) and pass with the fix; the 50 geo and shape suites, 424 tests, 0 failures with assertions on and off;checkGoogleJavaFormatpasses; the whole:lucene:core:test, 750 suites and 8,894 tests, 0 failures, three times (assertions from the seed, on, off). - Standalone runner: 5 of 5 pass with
-eaand-da; the reach probe prints 3 / 0 / 1 for all three query forms. Earlier, the independent reproducers' and reviewers' tests,TestPolygon2D(16 of 16) andTestXYPolygonShapeQueries(4 of 4) passed with the same fix.
Keep the current tests and add || (holes != null && holes.intersectsLine(...)) (and holes.intersectsTriangle(...)) to the pre-test: a shape that touches a hole but is not inside one hole must cross land. Closest in spirit to the pre-8.6 hole-first step. +7/-2.
Trade-offs
One extra hole traversal for every segment or triangle that fails the first tests (shapes inside a hole, or outside the polygon within its bounding box), so a little more work than the recommended option; no benchmark was run. Holes that touch at a vertex behave as with the recommended option (executed).
What the checks validated
Same worktree: the 50 geo and shape suites, 424 tests, 0 failures with assertions on and off; checkGoogleJavaFormat passes; the whole :lucene:core:test with assertions on, 8,894 tests, 0 failures; standalone 5 of 5 pass with -ea and -da, and the probe prints 3 / 0 / 1.
Use the 8.5.2-style hole-first relation for holed query polygons.
Trade-offs
Correct (8.5.2 is), but gives back the LUCENE-9273 speed-up for every holed query. Not run.
State that INTERSECTS and DISJOINT may be wrong for query polygons with holes.
Trade-offs
No javadoc limits holes today, 8.5.2 answered correctly, and the fix is small. Not recommended.
Products that pass polygons with holes to these queries, such as geo-shape search over regions with lakes or enclaves cut out, inherit the wrong matches until a fix is released and they upgrade; nothing wrong is stored, so a fix needs no reindex.
A bounded-exhaustive comparison of Lucene's shape queries with an exact geometry library (JTS) over small grid shapes, about 1.94 billion checks. With the already-known geo bugs patched out, the only remaining INTERSECTS misses were shapes whose vertices sat in a hole of the query polygon; the cause was then read from Polygon2D.
The method in full176 words
Method: bounded-exhaustive differential testing against an exact reference (the hunt's "semi-formal" tier). A hunt unit, "shape relations against an exact reference", 2026-10-27.
- Documents: lattice points, segments, triangles, quadrilaterals, 5-6-gons, polygons with a hole and multi-part documents; query shapes from the same lattices, on 6 x 6 grid-exact coordinates in 9 XY and LatLon families (including the dateline and the poles); all four relations, through the BKD shape index, shape doc values and the point fields.
- Reference: JTS 1.20 DE-9IM relations on the decoded coordinates, with strict and permissive boundary bands; about 1.94 billion checks.
- With the hunt's already-known geo fixes shadowed in, the only remaining INTERSECTS misses and DISJOINT false matches were documents whose vertices lie in or on a hole of the query polygon. The AI finder read the cause from
Polygon2D, confirmed it at theComponent2Dlevel, and reduced it to a public-API JUnit. - A narrower hole-boundary form had been noted earlier, untriaged, by a hunt verifier of another geo bug.
- Then independent AI tools reproduced, reviewed and judged it (see the checks).
No prior report found (best-effort search, 2026-10-27): two runs of the scripted prior-report search over GitHub, commits, downstream projects, mailing lists and JIRA; direct GitHub, JIRA, list and web searches; and all 409 open PRs. The nearest item is LUCENE-9273 (#10313), the change that introduced it.
Searches4 searches
No issue, PR, commit, JIRA entry, list thread or web page found reports this defect. Polygon2D.java on main, branch_10x and branch_10_6 is byte-identical to the tested copy, and no open PR touches it.
Nearest items, none of them this bug: #10313 (LUCENE-9273), the change that introduced it, which never mentions holes; #9782 (LUCENE-8736, open), WITHIN errors at shared boundaries on a convex polygon; #9481 (LUCENE-8435), which added polygon shape queries with the hole check and left the todo "add testing for arbitrary polygons with holes"; #10909 (LUCENE-9870), the same symptom in Circle2D, fixed; elastic/elasticsearch#9360 (2015), a false positive inside a query hole in an older prefix-tree filter, the opposite direction in a different engine.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene) | 2026-10-27 | two runs of a scripted prior-report search (51 and 56 queries, also covering downstream repositories, the lists and JIRA; 97 and 267 distinct hits, 0 rated high, the 36 medium hits read or triaged) plus 37 direct queries: polygon with holes, Polygon2D hole, intersectsLine, intersectsTriangle, between holes, two holes, LatLonShape / XYShape hole, LUCENE-9273 | no report; the items listed above |
| Open PRs | 2026-10-27 | the changed files of all 409 open apache/lucene PRs | none touches Polygon2D, EdgeTree, Component2D, ComponentTree, LatLonShapeQuery or XYShapeQuery |
| Downstream repositories | 2026-10-27 | 33 queries: Elasticsearch (17), OpenSearch, its geospatial plugin, Solr, lucene-solr, and searches across all repositories | only elastic/elasticsearch#9360 (2015), the opposite direction in an older engine |
| JIRA, mailing lists, web | 2026-10-27 | 10 JQL queries over LUCENE and SOLR; dev@, java-user@ and issues@ (7 queries each); 7 web queries | no report |
Introduced by LUCENE-9273 (f914e08b364, 2020-04-20), first released in 8.6.0. A probe fails on all 49 releases from 8.6.0 to 10.5.2 and passes on all 17 from 7.5.0 to 8.5.2; commit-level builds confirm the change on both branches. Not fixed upstream.
Versions tested5 rows
Introduced by LUCENE-9273 "Speed up geometry queries by specialising Component2D spatial operations" (lucene-solr PR #1341, issue now apache/lucene#10313; author iverase, approved by nknize), merged 2020-04-20 as f914e08b364 on master and a001f135f22 on branch_8x (byte-identical Polygon2D.java). It replaced Polygon2D.relateTriangle, which checked the holes first (a step from LUCENE-8435, 7.5.0), with the specialised intersectsLine / intersectsTriangle. Later commits on Polygon2D leave the two methods unchanged. Only the two-hole geometry was run on releases before 8.6.0.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | the brief's tests 4 of 5 fail (gradle and the standalone runner, assertions on and off); probe INTERSECTS=1, DISJOINT=2 |
| 10.0.0-10.5.2 (12), 9.0.0-9.12.3 (21), 8.6.0-8.11.4 (16) | affected | the release probe gives INTERSECTS=1, DISJOINT=2 for XYShape and LatLonShape on each; the brief's tests 4 of 5 fail on 10.5.2 and 9.12.3 |
| 7.5.0-8.5.2 (17) | not affected | INTERSECTS=3, DISJOINT=0 (7.5.0-8.1.1 with a LatLonShape-only port, before XYShape existed; 7.5.0-8.4.1 with lucene-sandbox, where the shape classes lived) |
f914e08b364 and a001f135f22 (LUCENE-9273) vs parents 37ad0e552dc, 62dd8ce8602 | bisect | both commits fail (1 / 2), both parents pass (3 / 0); commit-level builds of lucene/core |
| main + fix (either option) | not affected | all repros pass; the 50 geo and shape suites and the 8,894 lucene/core tests, 0 failed |
An independent reproducer, a devil's advocate, an adjudication and reviews by another model family (which ran their own tests) all found it real; the adjudication confirmed it with high confidence. Registry re-runs, a final audit and this doc's gradle runs reproduced it with new seeds.
Checks and verdicts474 words
Every verification check (all but the security assessment) wrote or ran its own test against the stock jars.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Public-API JUnit fails 2 of 2 on main (-ea, -da), 10.5.2 and 9.12.3; passes with the fix and still fails with the five other known geo fixes applied (re-run by the fact check with this doc's 5 tests: 4 of 5 fail). |
| cross-model review, refute lens | real (high) | An independent reviewer from another model family (OpenAI GPT-5.6) wrote its own test with another shell and holes and controls: 4 of 5 fail with -ea and -da; with the fix 5 of 5 pass, and so do TestPolygon2D and TestXYPolygonShapeQueries. |
| Independent reproducer | real | Own test from the claim only: 4 of 4 fail on main, 10.5.2 and 9.12.3; its 8.x port fails on 8.6.0 and passes on 8.5.2; passes with the fix. |
| Cross-model review, intended lens | real (high) | OpenAI GPT-5.6: the intended-behaviour argument is weak; its own test and main() fail on main and 8.6.0, pass on 8.5.2 and with the fix. |
| Devil's advocate | real | No doc, test or discussion makes the miss intended; added the U-shaped-hole and three-hole forms (4 of 4 fail on main, 10.5.2, 9.12.3) and corrected a note: the single-hole form needs no other fix. |
| adjudication | confirmed (high) | Re-ran the independent reproduction's test: 4 of 4 fail with -ea and -da, 4 of 4 pass with the fix. |
| security assessment | none | Wrong match sets only; no guarantee for untrusted input is broken; file publicly. |
| registry re-run and audit | kept | Follow-up re-run, seed 5EEDF00D: 2 of 2 fail with -ea and -da, 2 of 2 pass with the fix. A fourth independent audit failed it again with -da (seed A0D17C4E) and on 10.5.2, passed it with the fix, and re-read the code and the LUCENE-9273 parent. |
| brief runs | reproduced; both fixes validated | Gradle in a scratch worktree: 4 of 5 fail on stock; both fixes pass the new tests, the geo and shape suites and the whole lucene/core suite. |
| fact check of this doc | 3 wording errors, fixed | Re-ran the gradle command, the standalone runner (main -ea and -da, 10.5.2, both fixes), the reach probe and the 8.6.0 / 8.5.2 ports, and re-read every cited line: the bug, the mechanism and the history hold. Three overstatements (test scope, fix B's full-suite modes, distance to the boundaries) were corrected. |
| cross-model fact check | bug confirmed; patch caveat added | OpenAI GPT-5.6 (read-only): the core bug and the repro hold. It flagged that holes may touch at one vertex; a new probe showed both fixes then report a segment through the shared corner as intersecting (as 8.5.2 did), which is now an open question in the fixes section. |
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 409 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,186 words
- Registry title
- Polygon2D.intersectsLine/intersectsTriangle never test the query polygon's hole edges, so LatLonShape/XYShape INTERSECTS misses, and DISJOINT matches, a line or polygon whose vertices all lie in holes but which crosses the polygon between them
- Area
- geo / shape queries (Polygon2D intersectsLine/intersectsTriangle; LatLonShape and XYShape INTERSECTS and DISJOINT with a query polygon that has holes)
- Priority
- Medium, rank 39: Core LatLonShape/XYShape INTERSECTS and DISJOINT have silently returned wrong hits for holed query polygons since 8.6.0 on the default and most used relations.
- Impact class
- silent wrong results
- Impact note
- Applications that run LatLonShape or XYShape polygon queries with INTERSECTS or DISJOINT (default codec, IndexWriterConfig and searcher, any index size, every release from 8.6.0 to 10.5.2 and, by reading, 10.6.0) silently miss (INTERSECTS) or wrongly return (DISJOINT) indexed lines and polygons whose vertices all lie in the query polygon's holes while they cross polygon area between them; the query polygon needs two or more holes or one non-convex hole, point documents are not affected, and the error is geometric (degrees or whole units away from any boundary), not a precision effect.
- Summary
- You index a line or polygon with LatLonShape or XYShape and search with a query polygon that has holes, for example a region with two lakes cut out; a line from one lake to the other crosses the land between them, so INTERSECTS should return it and DISJOINT should not. Polygon2D.intersectsLine/intersectsTriangle report an intersection only when a vertex of the indexed shape is inside the polygon (holes excluded) or one of its edges crosses the outer ring; they never test the hole edges, so when every vertex lies in a hole the shape counts as not intersecting. The query returns the wrong documents with no error (repro: the square [0,10]^2 with holes [1,3]x[4,6] and [7,9]x[4,6]; of the line (2,5)-(8,5), the box [2,8]x[4.5,5.5] and the point (5,5), INTERSECTS returns only the point and DISJOINT returns the line and the box).
- Introduced
- f914e08b3647ad91eb97ada6ebefe1e96ebd5a89 (LUCENE-9273 'Speed up geometry queries by specialising Component2D spatial operations', apache/lucene-solr#1341, issue now apache/lucene#10313; author iverase, approved by nknize; branch_8x backport a001f135f229a4c08afe4b6eb3dc11da5db91605 with a byte-identical Polygon2D.java, the commit shipped in 8.6.0) [exec: commit-level builds of lucene/core on both branches, the parents 37ad0e552dcb and 62dd8ce86022 pass and both commits fail]. It replaced Polygon2D.relateTriangle, which checked the holes first (a hole relation of CROSSES returned CROSSES, a step from LUCENE-8435 in 7.5.0), with intersectsLine/intersectsTriangle, whose pre-test uses the hole-aware contains() and outer-ring crossings only. Later commits on Polygon2D (LUCENE-9570 reformat, LUCENE-9595, LUCENE-10003, LUCENE-10514, GITHUB#12655) leave these two methods unchanged [read: git blame]. (2020-04-20)
- First affected version
- 8.6.0 [exec] (8.5.2 is the last correct release [exec])
- Affected versions
- Every release from 8.6.0 to 10.5.2, 10.6.0 (by reading) and main a9779ef3c193; 7.5.0 to 8.5.2 are correct. [exec] a main() port of the repro geometry on all 66 lucene-core releases from 7.5.0 to 10.5.2 (SHA-1-checked release jars, -ea; 7.5.0-8.1.1 with a LatLonShape-only port because XYShape did not exist yet): 8.6.0-8.11.4 (16), 9.0.0-9.12.3 (21) and 10.0.0-10.5.2 (12) fail with XYShape and LatLonShape INTERSECTS=1 and DISJOINT=2 (expected 3 and 0), 7.5.0-8.5.2 (17) pass; at commit level the parents of f914e08b364 and a001f135f229 pass and both commits fail; the finder's JUnit fails 2 of 2 on 9.12.3, 10.5.2 and main (-ea and -da); the independent reproduction's test fails on main, 10.5.2 and 9.12.3 and its 8.x port fails on 8.6.0 and passes on 8.5.2; the intended-behaviour check's test (one non-convex hole, three holes) fails on main, 10.5.2 and 9.12.3. Only the two-hole geometry was executed on releases before 8.6.0. Not fixed upstream: Polygon2D.java on main, branch_10x and branch_10_6 is byte-identical to the tested copy (checked 2026-10-27). All runs with JDK 25 on aarch64.
- 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: exhaustive check
- Upstream and prior reports
- none found (the prior-report record, verdict new, searched 2026-10-27 plus, read-only: two scripted prior-report searches with 51 and 56 queries over GitHub issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists and the JIRA archive (97 and 267 distinct hits, 0 rated high, the 36 medium hits read or triaged); 37 direct apache/lucene GitHub queries, 33 downstream and cross-repository queries, 10 JQL queries, list-archive and web searches; the GraphQL list of all 409 open apache/lucene PRs with their changed files). Nearest items, not this bug: apache/lucene#10313 (LUCENE-9273, the change that introduced it, closed in 8.6.0; issue, PR and review never mention holes), #9782 (LUCENE-8736, open, WITHIN errors at shared boundaries on a convex polygon: different relation, trigger and code), #9481 (LUCENE-8435, added polygon shape queries with a hole check and a todo 'add testing for arbitrary polygons with holes'), #10909 (LUCENE-9870, the same method name and symptom in Circle2D, fixed), elastic/elasticsearch#9360 (2015, a false positive inside a query hole in an older prefix-tree filter: opposite direction, different engine). Not fixed upstream: Polygon2D.java on main, branch_10x and branch_10_6 is byte-identical to the tested copy, and none of the 409 open PRs touches Polygon2D, EdgeTree, Component2D, ComponentTree, LatLonShapeQuery or XYShapeQuery.
- Lucene commit tested
- main
a9779ef3c193(commit of 2026-10-26; tested 2026-10-27) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off; the 10.5.2 and 9.12.3 release jars (the brief's tests); a version-independent probe on all 66 release jars from 7.5.0 to 10.5.2, and commit-level builds of LUCENE-9273 and its parents on both branches. Both fix options were validated in the scratch worktree: the new tests, the 50 geo and shape suites with assertions on and off,checkGoogleJavaFormat, and the wholelucene/coretest suite (8,894 tests; option A three times, option B once). A probe with two holes that touch at a vertex ran on main, with each fix, and on 8.5.2, 8.6.0 and 10.5.2.
- Finding record
lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java:111-117 (hole-aware contains), :119-153 (relate checks holes first), :155-174 (intersectsLine), :176-201 (intersectsTriangle)lucene/core/src/java/org/apache/lucene/geo/Component2D.java:49 and :60 ('return true if this component2D intersects the provided line / triangle')lucene/core/src/java/org/apache/lucene/document/LatLonShape.java:295 and XYShape.java:228 (newPolygonQuery: 'find all indexed geo shapes / cartesian shapes that intersect a provided polygon')lucene/core/src/java/org/apache/lucene/document/LatLonShapeQuery.java:107, :117 and XYShapeQuery.java:89, :99 (INTERSECTS leaf predicates); SpatialQuery.java:129-130 (DISJOINT leaf = intersects().negate())
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java:45, :60, :111-117, :119-153, :155-174, :176-201, :369-386lucene/core/src/java/org/apache/lucene/geo/ComponentTree.java:112-150, :153-195lucene/core/src/java/org/apache/lucene/document/LatLonShapeQuery.java:99, :107, :117; XYShapeQuery.java:81, :89, :99; SpatialQuery.java:129-130
- Who is affected
lucene/core/src/java/org/apache/lucene/geo/Polygon.java:31-32 (holes counter-clockwise); document/LatLonShape.java:72 (precision warning)lucene/core/src/java/org/apache/lucene/document/LatLonShape.java:72-74, :263; XYShape.java:214; ShapeDocValues.java:913, :932
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/geo/Component2D.java:27, :49, :60lucene/core/src/java/org/apache/lucene/document/LatLonShape.java:72, :294-297; XYShape.java:227-230; geo/Polygon.java:31-35lucene/core/src/test/org/apache/lucene/document/BaseSpatialTestCase.java:747-797; geo/TestPolygon2D.java:50, :364; document/TestLatLonShape.java:201-234, :257-291
- Suggested fix
lucene/core/src/java/org/apache/lucene/geo/Tessellator.java:43-48; geo/ComponentTree.java:205-243; geo/EdgeTree.java:65-67
- TestXYShapeLineBetweenHoles.java (JUnit test):
repro/LFV-698/src/org/apache/lucene/document/TestXYShapeLineBetweenHoles.java(54 lines)
A light summary card of the repro, made in parallel with this doc from executed runs on main a9779ef3c193 and the code.
assets/LFV-698/bug.png(409 KB)
![Summary card. Title: XYShape / LatLonShape INTERSECTS misses a shape that crosses the query polygon only between its holes. Left: the query square [0,10] x [0,10] minus holes A and B, with the line (2,5)-(8,5) and the box [2,8] x [4.5,5.5] reaching from hole A to hole B (missed) and the point (5,5) on the land between them (found). Middle: s.count on main with assertions on and off, INTERSECTS expected 3, actual 1; DISJOINT expected 0, actual 2. Right: inside Polygon2D.intersectsLine for the line, contains(2,5) and contains(8,5) are false (in the holes), tree.crossesLine is false (outer ring only), the hole edges crossed at (3,5) and (7,5) are never tested, so it returns false; the box's two triangles fare the same. Bottom: what it needs, the introducing change LUCENE-9273 (8.6.0), the release sweep, and the fix, which tests the corners against the outer ring only and gives INTERSECTS 3, DISJOINT 0.](../assets/LFV-698/bug.png)