A geo3d path is a chain of segments, each closed at its ends by a cutoff plane across the path, and every plane test allows 1e-12. A point inside that margin passes a segment's test, but its projection onto the path lies a little farther from the plane (by 1/cos of the point's offset from the centre line, plus rounding) and can fail the same test, and the code then throws instead of answering.
GeoPathFactory.makeGeoPath(planet, halfWidth, points)builds a corridor along great-circle segments between the points. Each segment is bounded by two side planes and by two cutoff planes, one through each of its end points, square to the path.- Every membership test in geo3d is tolerant: a point up to 1e-12 (
Vector.MINIMUM_RESOLUTION) on the wrong side of a plane still counts as inside. computeDistanceasks every segment that contains the point. A segment drops the point perpendicularly onto its centre line (the foot) and returns the distance along the path to the foot plus the distance from the foot to the point. The path returns the smallest answer.
- Build a path on the unit sphere through (lat, lon) = (0, 0), (0, 0.1), (0, 0.2) with half-width 1e-4 rad: two segments that meet at longitude 0.1. Take a point 3.45e-6 rad beside the path and 1.0e-12 rad past that corner (on an Earth-sized sphere: 22 m beside it, 6 micrometres past).
path.isWithin(p)is true. - You expect
path.computeDistance(DistanceStyle.ARC, p)to be about 0.1000034: 0.1 along the path plus 3.45e-6 across. Points 1e-15 rad before or after it return 0.1000034486773084. - Instead it throws
RuntimeException: Can't find world intersection for point x=0.995004165272009 ..., with assertions on and off.computeNearestDistance,computePathCenterDistanceandcomputeDeltaDistancethrow too. - Why: every plane test allows 1e-12. Segment 1 (longitude 0 to 0.1) puts the point 9.99992e-13 past its end plane: inside. The point's foot on the centre line, at the same longitude, computes as 1.0000056e-12: outside; the other candidate lies on the far side of the globe. With no foot left the code throws, although the joint and segment 2, which also contain the point, would answer 0.1000034.
- The path sort is hit the same way: a path whose middle point sits at a stored document's decoded longitude makes
IndexSearcher.searchthrow. By accident practically never: at the repro point's offset one representable longitude throws (1.4e-17 rad), a few 1e-15 rad far off the centre line of a 500 km path, and natural-data probes found no hit.
Callers of the experimental geo3d path distance methods and of Geo3DDocValuesField.newPathSort: the call or the sorted search throws a RuntimeException, not a value; path queries are fine. The point must sit about 1e-12 rad past a cutoff plane, in a window at most a few 1e-15 rad wide: natural data never hit it, but a sort can target one document.
Mechanism, terms and numbers787 words
A geo3d path is a chain of great-circle segments with a half-width. Each segment is bounded by two side planes and two cutoff planes, one through each end point, and every plane test in geo3d counts a point up to 1e-12 on the wrong side as inside. To measure a distance, every segment that contains the point projects it onto its centre line: it intersects its great-circle plane with the plane through the point square to it, gets two candidate feet (the projection and its antipode) and keeps the one that passes both cutoff planes, else throws RuntimeException("Can't find world intersection ..."). The foot lies farther from the cutoff plane than the point (by 1/cos of the point's offset from the centre line; near the centre line rounding decides), so for a point at the very edge of the 1e-12 band of a cutoff plane the point passes and its foot does not, and computeDistance, computeDeltaDistance, computeNearestDistance and computePathCenterDistance throw, and so does IndexSearcher.search with Geo3DDocValuesField.newPathSort. In the repro, a point 3.45e-6 rad beside the path (0,0)-(0,0.1)-(0,0.2) and 1.0e-12 rad past its middle point evaluates to 9.99992e-13 on segment 1's end plane (inside) and its foot to 1.0000056e-12 (outside). The throw came with LUCENE-6578 (2015, first released in 5.3.0); 5.2.1 returns the distance. The call throws rather than returning a wrong value, and membership queries are not affected. The window is about 1e-12·b²/2 rad per band edge for a point at offset b, plus about 1e-17 rad of rounding (one representable longitude for the repro point, a few 1e-15 rad near the edge of a 500 km path), so it needs searched-for coordinates. Candidate fix: when neither foot passes, take the one on the point's side (to be checked on very wide paths).
- Cutoff plane: the plane through a segment's start or end point, square to the segment (
startCutoffPlane,endCutoffPlane,GeoStandardPath.java:1443-1446). On a straight path like the repro's, the end plane of one segment and the start plane of the next coincide; on a bent path they are two different planes through the same point (each contains its own segment's normal,:1443,:1446). - Tolerance:
SidedPlane.isWithinreturns true when|evaluate| < MINIMUM_RESOLUTION= 1e-12 (SidedPlane.java:300-308,Vector.java:26), so a point up to 1e-12 past a cutoff plane still belongs to the segment. - Foot: the intersection of the segment's great-circle plane with the plane through the point square to it. There are always two, antipodal; the code keeps the one that passes both cutoff planes.
computeDistancereturns +Infinity if the path does not contain the point, else asks the tree of segments and end caps; aPathNodeasks both children and takes the minimum (GeoBaseDistanceShape.java:46-53,GeoStandardPath.java:301-307,:679-686).- Segment 1 (longitude 0 to 0.1) contains the repro point by the tolerance: its end cutoff plane evaluates to 9.999917560676863E-13 (
PathSegment.isWithin,:1504-1510). pathDistancecomputes the two feet (:1740-1758). The real foot is at the point's own longitude, 0.10000000000100001, and evaluates to 1.0000056338554941E-12 on the same plane: outside. The other foot, at longitude -3.04, fails the start plane. Neither passes, so it throws (:1766-1774).- Why the foot fails: on this straight path, a point at offset b from the centre line and s past the plane evaluates to cos(b)·sin(s), its foot to sin(s). So even in exact arithmetic, points with sin(s) between 1e-12 and 1e-12/cos(b) pass while their foot fails: a window of about 1e-12·b²/2 rad per band edge, plus about 1e-17 rad of rounding. For the repro point (b = 3.45e-6) the geometric part is 6e-24, so rounding decides: the two values differ by 1.4e-17, one ulp of 0.1, and one representable longitude throws. Off the centre line of a 500 km path the window is geometric: 89 throwing longitudes (1.2e-15 rad) at b = 0.035, 355 (4.9e-15 rad) at b = 0.07 (fact-check sweep over every representable longitude).
- The exception leaves the tree, so neither the joint at the middle point (answers 0.1000034486773084) nor segment 2 (contains the point by a full 1e-12, answers 0.10000344864511553) is ever asked.
The same else-throw sits in pathCenterDistance (:1615), nearestPathDistance (:1663) and pathDeltaDistance (:1717) of GeoStandardPath, and in GeoDegeneratePath (:735, :790, :847), whose nearest and centre distances were shown to throw. The path sort reaches it through Geo3DPointDistanceComparator.copy -> computeMinimumDistance -> computeDistance for each document value (Geo3DPointDistanceComparator.java:137-138, :163-183). Not the same bug: newOutsidePathSort uses computeOutsideDistance, which returns 0 for a contained point and otherwise measures distances to the segments' planes and corners, with no foot choice (GeoBaseMembershipShape.java:46-53, GeoStandardPath.java:1783-1838).
Exposure and likelihood276 words
- Who is hit. Callers of
GeoPath.computeDistance,computeDeltaDistance,computeNearestDistanceandcomputePathCenterDistance(geom API,@lucene.experimental), and searches sorted byGeo3DDocValuesField.newPathSort. No upstream test callsnewPathSort. Membership queries (Geo3DPoint.newPathQuery) are not affected. - What happens. A
RuntimeExceptionfrom an explicitthrowinstead of the distance. With the sort,IndexSearcher.searchfails for the whole request. No wrong value, hang, extra resource use or index change was observed. - What it takes. A point about 1e-12 rad (6 micrometres on Earth) past a segment's cutoff plane, inside a window about 1e-12·b²/2 rad wide per band edge for a point at offset b from the centre line, plus about 1e-17 rad of rounding: one representable longitude (1.4e-17 rad) for the repro point, about 6e-16 rad for the sort repro's document, a few 1e-15 rad near the edge of a 500 km path. At interior path points and at a path's end point.
- How likely. By accident practically never: 0 of 200,000 points placed exactly at path vertices and 0 of 200,500 grid points threw; uniform sampling within ±3e-12 rad of a vertex gave about 0 to 5e-6. Sweeps that sample only the band find a few: 10-12 per 1.92 million points on SPHERE, 6 on WGS84; 14 per 800,001 at one of seven offsets on a bent path; 12 per 3.6 million on a straight path (hit counts on each sweep's own lattice, not rates for real data). On purpose, a path can be chosen so that sorting a given stored document throws every time.
- Default vs opt-in. Opt-in: the spatial3d module and its geom API, or the path sort. Impact class crash, likelihood contrived, priority Low.
The test shows that GeoPath.computeDistance throws for a point the path itself reports as within, when the point sits at the edge of the 1e-12 tolerance band of a segment's cutoff plane (ulp-chosen point on a SPHERE path).
/*
* 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.spatial3d.geom;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestGeoPathDistanceAtCutoffPlane extends LuceneTestCase {
public void testDistanceOfPointInsidePath() {
PlanetModel pm = PlanetModel.SPHERE;
GeoPath path =
GeoPathFactory.makeGeoPath(
pm,
1e-4,
new GeoPoint[] {
new GeoPoint(pm, 0, 0), new GeoPoint(pm, 0, 0.1), new GeoPoint(pm, 0, 0.2)
});
GeoPoint p = new GeoPoint(pm, -3.448684587613504E-6, 0.10000000000100001);
assertTrue(path.isWithin(p));
// expected: a distance of about 0.1 + 3.4e-6; actual: RuntimeException "Can't find world intersection"
assertEquals(0.1 + 3.448684587613504E-6, path.computeDistance(DistanceStyle.ARC, p), 1e-9);
}
}
# in a copy of this finding's repro folder (repro/LFV-742/, 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/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.spatial3d.geom.TestGeoPathDistanceAtCutoffPlane
- Expected vs actual
- Expected an arc distance of about 0.1 + 3.4487e-6; actual
RuntimeException: Can't find world intersection for point ...fromGeoStandardPath$PathSegment.pathDistance(-ea and -da).
More files, environment, full output44 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.RuntimeException: Can't find world intersection for point x=0.995004165272009 y=0.09983341664722949 z=-3.448684587606668E-6
org.apache.lucene.spatial3d.geom.GeoStandardPath$PathSegment.pathDistance(GeoStandardPath.java:1773)
org.apache.lucene.spatial3d.geom.GeoStandardPath$PathSegment.distance(GeoStandardPath.java:1544)
org.apache.lucene.spatial3d.geom.GeoStandardPath$PathNode.distance(GeoStandardPath.java:685)
org.apache.lucene.spatial3d.geom.GeoStandardPath$PathNode.distance(GeoStandardPath.java:684)
org.apache.lucene.spatial3d.geom.GeoStandardPath$PathNode.distance(GeoStandardPath.java:684)
org.apache.lucene.spatial3d.geom.GeoStandardPath.distance(GeoStandardPath.java:306)
org.apache.lucene.spatial3d.geom.GeoBaseDistanceShape.computeDistance(GeoBaseDistanceShape.java:52)
org.apache.lucene.spatial3d.geom.GeoBaseDistanceShape.computeDistance(GeoBaseDistanceShape.java:43)
org.apache.lucene.spatial3d.geom.TestGeoPathDistanceAtCutoffPlane.testDistanceOfPointInsidePath(TestGeoPathDistanceAtCutoffPlane.java:35)GeoDistance.computeDistancedocuments a distance, or +Infinity outside the shape; no exception. The path reports this point as inside.- Points 1e-15 rad either side get the distance;
TestGeoPathexpects a distance or +Infinity on the same three points. - Before LUCENE-6578 the code could not throw (5.2.1 returns 0.1000034 here); a similar tolerance exception, #9292, was fixed as a bug.
The full argument235 words
- Contract.
GeoDistance: "The following methods compute distances from the shape to a point expected to be INSIDE the shape. Typically a value of Double.POSITIVE_INFINITY is returned for points that happen to be outside the shape." andcomputeDistance"@return the distance." (GeoDistance.java:28-30,:45-57).GeoPath: the nearest distance is computed "to a point that doesn't need to be inside the shape" (GeoPath.java:26-28); the centre distance is documented as "the shortest distance from the path center to the point" (GeoPath.java:57-80). The internalpathCenterDistancejavadocs promise +Infinity outside a segment (GeoStandardPath.java:565-566,GeoDegeneratePath.java:695).newPathSortdocuments only anIllegalArgumentExceptionfor a null field or invalid coordinates (Geo3DDocValuesField.java:214). - The shape says the point is inside (
path.isWithin(p)is true), and its neighbours 1e-15 rad away get a distance. - Tests.
TestGeoPath.testPathDistanceuses the same three-point path (half-width 0.1) and expects a distance (for example0.15 + 0.05) or +Infinity (TestGeoPath.java:22-43). - History. Before LUCENE-6578 the segment code took the other foot when the first failed a cutoff plane and could not throw; LUCENE-6578 added the strict check and the throw while the tolerance already existed. 5.2.1 returns 0.1000034487 for this point. None of the upstream discussions that added or copied the throw (#7636, #9011, #9004, #9087) or added the sort (#8267) mentions it.
- Precedent. #9292 (LUCENE-8245), a boundary-tolerance exception in
GeoComplexPolygon, was fixed as a bug.
The devil's-advocate check228 words
The strongest case for "intended" (rated weak by the lenses that built it; rated moderate by a later cross-model fact check, which added the last two points): the throw is a deliberate fail-fast guard in an experimental API at the limit of numerical resolution; geo3d already turns fragile geometry into exceptions (#8411 "Ambiguous boundary points; path too short", #7817 tiny circles); the test passes slightly off-unit coordinates; the guard may protect wide-path geometry.
Why it fails: those are construction-time IllegalArgumentExceptions that reject a degenerate shape. Here the shape is ordinary, it reports the point as inside, and the joint and the neighbouring segment have the answer. The guard's assumption (a point inside the section has a foot inside the section) is wrong by construction, not only at the limit of numerical resolution: in exact arithmetic the foot is 1/cos(b) farther from the cutoff plane than a point at offset b, so off-centre points throw with no rounding involved. The test's coordinates are the doubles GeoPoint(pm, lat, lon) builds on this JVM, off the unit sphere only by rounding like every GeoPoint, and the latitude/longitude form throws the same. That the fallback reaches farther on very wide paths is a reason to review a fix, not for a contained point to throw. "Experimental" allows API changes, not undocumented runtime failures. Verdict: a defect, of low priority.
Recommended A. Take the foot on the point's side instead of throwing (validated on the reproduced cases and the spatial3d suite)
A candidate: when neither foot passes the tolerant cutoff test, use the one on the point's side (the larger dot product with the point) instead of throwing, at all 7 copies (4 in GeoStandardPath, 3 in GeoDegeneratePath). The repro then passes, as do all 182 spatial3d tests and the static checks. Very wide paths still need a maintainer's look.
} else if (startCutoffPlane.isWithin(intersectionPoints[1])
&& endCutoffPlane.isWithin(intersectionPoints[1])) {
thePoint = intersectionPoints[1];
} else {
- throw new RuntimeException(
- "Can't find world intersection for point x=" + x + " y=" + y + " z=" + z);
+ // The point passed the tolerant section test, but rounding put both feet just outside
+ // a cutoff plane: use the foot on the point's side of the world.
+ thePoint =
+ intersectionPoints[0].dotProduct(x, y, z) >= intersectionPoints[1].dotProduct(x, y, z)
+ ? intersectionPoints[0]
+ : intersectionPoints[1];
}All options and trade-offs3 options · 597 words
Fix A was tested through gradle in a scratch worktree of 645d4fe14a20, on the reproduced cases and the spatial3d suite; it was not tested on very wide paths. B and C are alternatives that were not run.
A. Take the foot on the point's side instead of throwing (validated on the reproduced cases and the spatial3d suite) (recommended)
The same hunk replaces the else-throw after the foot choice in pathDistance, pathCenterDistance, nearestPathDistance and pathDeltaDistance of GeoStandardPath and in the three GeoDegeneratePath methods (+42/-14 lines in two files). It is the study's fix sketch with a neutral comment.
} else if (startCutoffPlane.isWithin(intersectionPoints[1])
&& endCutoffPlane.isWithin(intersectionPoints[1])) {
thePoint = intersectionPoints[1];
} else {
- throw new RuntimeException(
- "Can't find world intersection for point x=" + x + " y=" + y + " z=" + z);
+ // The point passed the tolerant section test, but rounding put both feet just outside
+ // a cutoff plane: use the foot on the point's side of the world.
+ thePoint =
+ intersectionPoints[0].dotProduct(x, y, z) >= intersectionPoints[1].dotProduct(x, y, z)
+ ? intersectionPoints[0]
+ : intersectionPoints[1];
}Trade-offs
- Changes behaviour only where the code throws today; no extra cost on the normal path.
- How far outside the segment the foot used can be depends on the point's offset b: up to about 1e-12/cos(b) rad past the cutoff plane. For ordinary widths that is about 1e-12 (in the repro segment 1 then answers 1.0e-12 above segment 2, and the path's minimum returns segment 2's value). For points far off the centre line of a very wide path (half-widths up to π/2 are accepted) it is not small: at cos(b) ≈ 5e-10 an overshoot of 1e-3 rad still evaluates inside the band (a cross-model fact check's analysis, not run). Check wide-path, near-pole and bent-path cases before adopting A as is.
- The two feet are antipodal, so for a point near the path their dot products have opposite signs and the choice is unambiguous.
- Keeps the separate
intersectionPoints.length == 0throws. Seven copies of one block: a small shared helper may read better.
What the checks validated
- Gradle, scratch worktree of
645d4fe14a20, seedDEADBEEF, 2026-11-01: without the fix the repro fails with assertions on and off; with it, it passes with assertions on and off (the study's latitude/longitude form too). ./gradlew :lucene:spatial3d:testwith the fix: 182 tests (the whole module plus the study's latitude/longitude form of the repro; the exact-coordinate test ran separately), 0 failures, 3 skipped../gradlew :lucene:spatial3d:check -x testwith the fix and the repro as a new test file: passes (google-java-format, ecj lint, forbidden APIs). The full Lucene test suite was not run.- Earlier: with the study's version of this fix shadowed into the stock jars, the finder's, the independent verifier's, the adjudication's and the cross-model reviewers' own tests pass.
Drop the cutoff tests on the feet and always take the intersection with the larger dot product. For a point inside a segment's corridor this is the same foot, since the other is its antipode.
Trade-offs
Removes the throw and two plane tests per call, but touches the choice for every point, not only the failing branch; not run.
Treat the point as outside this segment when neither foot passes, as the internal javadoc describes, and let the neighbouring segment or end cap answer.
Trade-offs
If no other component claims the point, computeDistance returns +Infinity for a point the path contains: a silent wrong value instead of an exception. Not run.
Applications that build newPathSort from user-supplied path coordinates can see IndexSearcher.search fail with this RuntimeException for coordinates chosen to the last bits; only that request fails.
Semi-formal: an AI tool built a bounded-exhaustive checker that compared geo3d's distance functions with an independent reference; it flagged a few RuntimeExceptions on a zero-width path next to a path point. The finder then located the window with a targeted sweep around path points and an ulp search, and reduced it to a public-API test.
The method in full184 words
Method: bounded-exhaustive translation validation of the real jar against an independent reference, in a study unit on the geo3d distance functions (main a9779ef3c193, 2026-10-27).
- The harness compared the path, circle and other shape distances with exact reference formulas over grids of shapes and points. In the path family it reported one exception class: 5
RuntimeExceptions on a straight zero-width path at a lateral offset of 1e-9 next to an interior point. - A targeted random sweep around path points then found 5 to 55 throwing points per 200,000 sampled within ±2e-12 rad of the cutoff plane on SPHERE, for widths 1e-6 to 1e-2 (on a 1e-15 lattice of along-path offsets that lands on the band edges: a hit count, not a density or a width); an ulp search found a path-point longitude that puts a stored document's decoded point in the window, for the sort repro.
- The finder wrote a three-test JUnit (SPHERE distance, zero-width nearest distance, WGS84 path sort); the one-test minimal repro was reduced from its first test, and this doc's test gives the same doubles as (x, y, z) coordinates.
No prior report found in GitHub issues, PRs and commits, the 153 spatial3d issues with their comments, the open PRs, the Lucene mailing lists, the JIRA archive, downstream projects or the web (searched 2026-10-27, re-checked 2026-11-01). The exception text appears in no issue or PR. Closest: #9292, a tolerance exception in another shape, fixed as a bug.
Searches5 searches
Closest items, none a report of this bug: #7636 (LUCENE-6578, introduced the throw), #9011, #9004 and #9087 (copied it; none of the discussions mentions it); #8267 (LUCENE-7212, open: added the path sort; 49 comments, no exception); #9742 (LUCENE-8696, a path/solid relationship bug); #9292 (LUCENE-8245, a boundary-tolerance exception in GeoComplexPolygon, fixed as a bug); #13056 (open, a membership mismatch for an off-surface quantized point). Upstream the exception text appears only in commit diffs and as diff context in one 2022 dev@ thread about dead code. Gaps: only 4 web searches; GitHub search does not index PR diffs.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits; all of GitHub | 2026-10-27 | two scripted prior-report searches (127 planned queries: class, method and exception terms; symptom and usage terms); 68 manual searches over apache/lucene, all of GitHub, Elasticsearch, OpenSearch, Solr and spatial4j; 1 code search | "Can't find world intersection": 0 issues or PRs anywhere; "world intersection" in apache/lucene: other shapes only |
| spatial3d issues and open PRs | 2026-10-27 | 153 spatial3d issues fetched with all comments and grepped for the exception text and the stack frames; changed files of all 410 open PRs | no match; no open PR touches GeoStandardPath.java or GeoDegeneratePath.java |
| Mailing lists, JIRA archive, web | 2026-10-27 | 57 direct searches over java-user@, dev@, issues@, builds@, commits@ and others; 8 JIRA queries (LUCENE and SOLR); 4 web searches | only commit diffs and one 2022 dev@ thread quoting the code as diff context |
| Git history | 2026-10-27 | git log -S on the exception text and on the foot check; the five discussions that added or copied the throw read in full | LUCENE-6578 added it; LUCENE-7962, LUCENE-7955 and LUCENE-8039 copied it; nobody discussed it |
| Re-check: apache/lucene issues and PRs; upstream branches | 2026-11-01 | read-only GitHub search ("world intersection", open PRs matching GeoStandardPath, issues updated since 2026-10-26), cut short by GitHub's search rate limit and not recorded; a repeat hit the limit too; file contents on main 2f424fd428da, branch_11x and branch_10_6 | nothing new; the throw sites are unchanged on all three branches |
Introduced by LUCENE-6578 (688c67f8ff22, branch_5x 780e40402387, 2015-07-01), first released in 5.3.0; copied into more methods and the zero-width path in 2017; the path sort has it since 6.1.0. 5.2.1 is clean; 14 releases from 5.3.0 to 10.6.0 executed, all affected. Not fixed upstream as of 2026-10-30.
Versions tested7 rows
Introduced by LUCENE-6578 "Geo3D: compute the distance from a point to a shape" (#7636; SVN era, patches on JIRA): trunk 688c67f8ff22 and branch_5x 780e40402387, both 2015-07-01. It replaced segment code that could not throw with a strict "within both cutoff planes" foot choice and an else-throw; the 1e-12 tolerance already existed. Copied into the nearest and centre distances (LUCENE-7962, #9011) and GeoDegeneratePath (LUCENE-7955, #9004), 2017-09, released in 7.1.0, and into pathDeltaDistance (LUCENE-8039, #9087, 2017-11-08), 7.2.0. The path sort (LUCENE-7212, #8267) shipped in 6.1.0. git blame on main resolves to the 2021 reformat 2cbf261032d and the 2016 file split e6fd37c4a16.
The study's old-release runs used each release's own API and latitude/longitude inputs on this JVM; on another platform the exact window can move by a last-bit trig difference, not the code path.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the repro throws at pathDistance:1773 with assertions on and off (standalone and gradle) |
| 10.6.0, 10.5.2, 9.12.3 | affected | the same test throws (:1775, :1775, :1783) |
| 5.3.0, 5.5.5, 6.0.0, 6.6.6, 7.7.3, 8.11.4, 9.4.2, 9.5.0 | affected | the distance throws at the foot choice (release-specific programs) |
| 6.1.0 | path sort | IndexSearcher.search with newPathSort throws from the sort's first release |
| 7.1.0, 7.2.0 | affected | zero-width nearest and centre distances (7.1.0); all four GeoStandardPath methods (7.2.0) |
| 5.2.1 | not affected | returns the distance 0.1000034487 for the same point |
fix A on 645d4fe14a20 | fix | the repro passes; 182 spatial3d tests pass |
Confirmed by the study's judge and independent verifiers (one from another model family, OpenAI GPT-5.6), a cross-model reviewer (OpenAI GPT-5.6) that ran its own test, an audit, and this doc's runs: the portable test fails on main and three releases and passes with the fix, which passes the whole spatial3d suite.
Checks and verdicts355 words
The study's checks: finder, a cross-model review, verifiers, an adjudication, a follow-up re-run and an audit; then this doc's portable repro, gradle runs and fix validation on 645d4fe14a20.
| Check | Verdict | What it found |
|---|---|---|
| finder | 3 of 3 fail | SPHERE distance, zero-width nearest distance and a WGS84 path sort throw on main, 10.5.2 and 9.12.3; pass with the fix sketch. |
| cross-model review, refute lens (OpenAI GPT-5.6) | real, high | Own test: an isWithin-true point throws at :1773; the nearest, centre and delta distances throw; the fix passes. Its own zero-width points did not throw (shown later by others). |
| Independent reproducer | real | Own inputs: 5 of 5 fail on main, 10.5.2 and 9.12.3, including an end-to-end path sort; its own fix (take the nearer intersection) passes. |
| Cross-model adversary, intended lens (OpenAI GPT-5.6) | real, high | Intended-behaviour argument weak; finite values 1e-9 rad either side, +Infinity for a truly outside point. |
| Intended-behaviour devil's advocate | real, high | Built and rejected the fail-fast reading; bent paths throw too; natural data does not. |
| adjudication | confirmed, high | 10 of 10 tests fail with assertions on and off and pass with the fix; its own probe found the throw at a path's end point too. |
| audit | confirmed | Re-ran the finder's test; its own scan finds 12 throwing contained points per 3.6 million on a straight SPHERE path. |
| this doc | confirmed; repro made portable | Found that the latitude/longitude repro depends on the last bit of Math.cos(0.1); the exact-vector test fails on main, 10.6.0, 10.5.2 and 9.12.3 and passes with fix A, which passes 182 spatial3d tests and the static checks. |
| fact check | real; docs corrected | Re-ran the test (standalone and gradle; main, 10.6.0, 10.5.2, 9.12.3, fix A) and every value; corrected the cause and window width (the foot is 1/cos(b) farther from the plane), the GeoPath comment's scope and the coinciding cutoff planes. |
| cross-model fact check (OpenAI GPT-5.6) | real; fix A bound qualified | Bug real; flagged fix A's bound on very wide paths, the 'exact unit vectors' wording and platform claim, the joint and newOutsidePathSort. |
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 492 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,581 words
- Registry title
- geo3d path distance methods (GeoStandardPath, GeoDegeneratePath) and Geo3DDocValuesField.newPathSort throw RuntimeException "Can't find world intersection" for a point inside the path that sits at the edge of the 1e-12 tolerance band of a segment's cutoff plane
- Area
- spatial3d / geom path distances (GeoStandardPath and GeoDegeneratePath PathSegment foot selection; Geo3DDocValuesField.newPathSort)
- Priority
- Low, rank 1136: A loud RuntimeException from experimental spatial3d path distances and the path sort, with no wrong results, reachable only at ulp-searched points inside a 1e-15 rad band around a tolerance (natural-data probes found no hits).
- Impact class
- crash
- Impact note
- Only callers of the spatial3d path distance methods (GeoPath.computeDistance, computeDeltaDistance, computeNearestDistance, computePathCenterDistance) or of the path sort Geo3DDocValuesField.newPathSort are exposed, with no special setting and no scale: for a point that the path itself reports as within, but that lies in a window of about 1e-16 to 1e-15 rad at about 1e-12 rad from a segment's cutoff plane, the call (or IndexSearcher.search with the path sort) throws a RuntimeException instead of returning a distance; there are no wrong results, membership queries (Geo3DPoint.newPathQuery) are not affected, natural data was never seen to hit the window, and a path can be chosen so that sorting a given document throws every time.
- Summary
- You build a geo3d path (GeoPathFactory.makeGeoPath, or the path of Geo3DDocValuesField.newPathSort) and ask for the distance of a point that the path reports as inside it, expecting a number (GeoDistance.computeDistance documents a distance, or +Infinity outside the shape). Instead, when the point sits at the very edge of the 1e-12 tolerance band of a segment's cutoff plane, the call throws RuntimeException("Can't find world intersection ..."): on the unit sphere a point 3.4e-6 rad beside a path of width 1e-4 and 1e-12 rad past its middle point throws instead of returning about 0.1000034, the same happens for a zero-width path's nearest and centre distance, and a 500 km path sort whose middle point sits at a stored document's decoded longitude makes IndexSearcher.search throw. The cause: the segment's tolerant section test accepts the point, then the same tolerant test rejects both rounded perpendicular feet of the point on the path, and the code throws instead of picking one.
- Introduced
- 688c67f8ff22b72b9df4ed2f92d26b909563b2b1 on trunk (svn r1688545) and its branch_5x backport 780e4040238713c2ff85c4b981cbc6b80aa4094f (svn r1688546), 'LUCENE-6578: Geo3D: compute the distance from a point to a shape' (JIRA LUCENE-6578, now apache/lucene#7636; svn era, no pull request; committed by dsmiley, CHANGES credits DaddyWri (Karl Wright) and David Smiley). It added computeDistance and PathSegment.pathDistance with findIntersections, a strict 'within both cutoff planes' choice of the foot and the else-throw, while SidedPlane.isWithin already accepted points up to MINIMUM_RESOLUTION outside a plane. The same block was later copied into nearestPathDistance and pathCenterDistance (LUCENE-7962, #9011, 2017-09), into GeoDegeneratePath (LUCENE-7955, #9004, 2017-09) and into pathDeltaDistance (LUCENE-8039, #9087, 2017-11-08); the user-facing newPathSort came with LUCENE-7212 (#8267, 2016-05). [read: git log -S on the exception text and on the foot check lists only these commits plus the LUCENE-7176 file split e6fd37c4a16; blame on main resolves to the LUCENE-9570 reformat 2cbf261032d and e6fd37c4a16; exec: 5.2.1 returns the correct distance, 5.3.0 throws] (2015-07-01 (trunk and branch_5x); facets: newPathSort 2016-05-17, nearest/centre distance and GeoDegeneratePath 2017-09, computeDeltaDistance 2017-11-08)
- First affected version
- 5.3.0 [exec] (tag dated 2015-08-20) for computeDistance; 5.2.1, the last release before, returns the correct distance 0.10000344868558741 for the same point [exec]. Per facet: Geo3DDocValuesField.newPathSort from its first release, 6.1.0 [exec: IndexSearcher.search throws]; computeNearestDistance / computePathCenterDistance and the zero-width GeoDegeneratePath from 7.1.0 [exec] (7.0.1 rejects width 0 and has no such methods [exec]); computeDeltaDistance from 7.2.0 [exec]
- Affected versions
- every release from 5.3.0 through 10.5.2 (the last release before 10.6.0), 10.6.0 [exec: the one-test repro fails on the release jars, SHA-1 checked against Maven Central, -ea and -da, 2026-11-01] and main a9779ef3c193. Executed (JDK 25, -ea unless noted, version-specific public-API main() probes or the finder's JUnit), all failing at the foot-selection throw and never at the 'no intersection' throw: 5.3.0, 5.5.5, 6.0.0, 6.1.0 (computeDistance and the path sort), 6.6.6, 7.1.0 (zero-width nearest/centre), 7.2.0 (all four methods), 7.7.3, 8.11.4, 9.4.2, 9.5.0 (both sides of the 2022 GeoStandardPath refactor), 9.12.3 and 10.5.2 (finder JUnit 3 of 3; the independent reproduction's JUnit 5 of 5 with -ea and -da), main (-ea and -da). Releases not executed are inferred from unchanged throw logic [read]. Not fixed upstream: upstream main a95c754ee003, branch_10x and branch_10_6 carry the same throw sites and no open PR touches the two files [read]. Not affected: 5.2.1 and earlier [exec 5.2.1]. Caveat: all runs used JDK 25; the JDKs the old releases shipped for could move the exact input window by last-ulp trig differences, not the code path.
- 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 (2026-10-27, read-only): scripted prior-report searches A and B (127 planned queries over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists and the JIRA archive) plus the finder's and the intended-behaviour check's earlier runs re-read; 68 manual GitHub issue/PR searches (apache/lucene, all of GitHub, Elasticsearch, OpenSearch, Solr, spatial4j); the 134 module:spatial3d issues plus 19 more (153) fetched with all comments and grepped for the exception text and the stack frames; the changed files of all 410 open apache/lucene PRs; 1 GitHub code search; 57 direct list searches; 8 JIRA queries (LUCENE and SOLR); 4 web searches; git log -S on the exception text. The message "Can't find world intersection" is in no GitHub issue, PR or JIRA issue (0 GitHub results); upstream it appears only in commit diffs and as diff context in one 2022 dev@ thread about dead code. Related, not reports: #7636 (LUCENE-6578, origin of the throw), #9011, #9004 and #9087 (copied it into new methods; none of the five discussions mentions it), #8267 (LUCENE-7212, open; added newPathSort, no exception discussed in 49 comments), #9742 (LUCENE-8696, a path/XYZSolid relationship bug), #9292 (LUCENE-8245, a boundary-tolerance exception in GeoComplexPolygon, fixed as a bug: precedent), #13056 (open; a membership mismatch for an off-surface quantized point, LFV-209). Not fixed upstream: GeoStandardPath.java and GeoDegeneratePath.java last changed on main in dfd536186fe (#15889, 2026-04-01, an instanceof cleanup), and no open PR touches them. Gaps: only 4 web searches.
- Lucene commit tested
- main
645d4fe14a20: the one-test repro (exact (x, y, z) coordinates) with assertions on and off, standalone and through gradle, and passing with the recommended fix; the fix in a scratch worktree with the whole spatial3d test suite (182 tests) and:lucene:spatial3d:check -x test. Release jars 10.6.0, 10.5.2 and 9.12.3 with the same test; the study's latitude/longitude form on 10.6.0. The study ran on maina9779ef3c193(spatial3d sources identical to645d4fe14a20) and executed 5.2.1 (clean) and 13 releases from 5.3.0 to 10.5.2. Upstream main2f424fd428da(2026-10-30) has the same spatial3d sources (read).
- Finding record
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoStandardPath.java:1504-1510 (at a9779ef3c193) (PathSegment.isWithin: startCutoffPlane, endCutoffPlane and the two connecting planes, each SidedPlane.isWithin) and :1517-1525 (isWithinSection: the two cutoff planes)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoStandardPath.java:1757-1775 (at a9779ef3c193) (pathDistance: findIntersections of the connecting and perpendicular planes; keep the intersection that passes both cutoff planes, else throw RuntimeException("Can't find world intersection ...") at :1773-1774; the 'no intersection' throw is :1761-1762); the same else-throw at :1615-1616 (pathCenterDistance, from :1574), :1663-1664 (nearestPathDistance, from :1623) and :1717-1718 (pathDeltaDistance, from :1672); :565 (PathComponent javadoc: 'the distance metric, or POSITIVE_INFINITY if the point is not within the bounds of' the segment); :34 (@lucene.internal)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegeneratePath.java:735-736, (at a9779ef3c193) :790-791 and :847-848 (the same else-throw in the zero-width PathSegment's pathCenterDistance, nearestPathDistance and pathDistance; :723-724, :778-779 and :835-836 are the 'no intersection' throws; only the nearest and centre distances were shown to throw), :695 (pathCenterDistance javadoc: 'or Double.POSITIVE_INFINITY if outside this segment'), :207 (computeNearestDistance calls segment.pathCenterDistance), :33 (@lucene.internal)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/SidedPlane.java:300-308 (at a9779ef3c193) (isWithin returns true when |evaluate| < MINIMUM_RESOLUTION, i.e. 1e-12 outside the plane still counts as inside)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDistance.java:28-57 (at a9779ef3c193) (computeDistance: '@return the distance', POSITIVE_INFINITY for points outside the shape; no exception documented; @lucene.experimental at :24)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBaseDistanceShape.java:46-53 (at a9779ef3c193) (computeDistance returns +Infinity when !isWithin, else distance())lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPath.java:22-28 (at a9779ef3c193) (@lucene.experimental; nearest and centre distances are computed 'to a point that doesn't need to be inside the shape')lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPathFactory.java:38-44 (at a9779ef3c193) (makeGeoPath returns GeoDegeneratePath below MINIMUM_ANGULAR_RESOLUTION, else GeoStandardPath)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DDocValuesField.java:194-225 (at a9779ef3c193) (newPathSort: hits carry the distance in meters; the only documented exception is IllegalArgumentException for a null field or invalid coordinates)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPointDistanceComparator.java:137-138 (at a9779ef3c193) (copy calls computeMinimumDistance) and :163-183 (computeMinimumDistance calls distanceShape.computeDistance for each document value)
- The finding in 30 seconds
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoStandardPath.java:30, :132-149, :301-307, :679-686, :1436-1446, :1504-1510, :1538-1549, :1727-1781, :1615-1616, :1663-1664, :1717-1718 (at 645d4fe14a20)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegeneratePath.java:735-736, :790-791, :847-848 (at 645d4fe14a20)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/SidedPlane.java:300-308; Vector.java:26; GeoBaseDistanceShape.java:46-53; GeoPathFactory.java:38-44 (at 645d4fe14a20)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPointDistanceComparator.java:137-138, :163-183 (at 645d4fe14a20)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DDocValuesField.java:365-372; Geo3DPointOutsideDistanceComparator.java:133-151; geom/GeoBaseMembershipShape.java:46-53; geom/GeoStandardPath.java:1783-1838 (newOutsidePathSort, not affected; at 645d4fe14a20)
- Who is affected
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPath.java:22; GeoDistance.java:24 (at 645d4fe14a20)
- Why it looks unintended
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDistance.java:28-30, :45-57; GeoPath.java:26-28, :57-80; GeoStandardPath.java:565-566; GeoDegeneratePath.java:695 (at 645d4fe14a20)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DDocValuesField.java:194-225 (at 645d4fe14a20)lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestGeoPath.java:22-43 (at 645d4fe14a20)
- History
a Lucene clone: releases/lucene/10.6.0, origin/main 2f424fd428da, origin/branch_11x 603af8703753, origin/branch_10_6 7e5aec4370e0 (throw sites read)
- TestGeoPathDistanceAtCutoffPlane.java (JUnit test):
repro/LFV-742/src/org/apache/lucene/spatial3d/geom/TestGeoPathDistanceAtCutoffPlane.java(37 lines)
Made in parallel with this doc from the mechanism in the brief; the PNG is the summary frame.
assets/LFV-742/bug.png(492 KB)
