A geo3d path is a corridor plus a round cap at each end. On WGS84 the cap at an end whose segment runs north-south is centred about 21 km (at 45N) toward the equator, so the poleward cap falls short and the equatorward cap overshoots.
- You index points with
Geo3DPointon WGS84 and search the path (44N,20E) to (45N,20E) withGeo3DPoint.newPathQuery(..., 20000, WGS84), sorting withGeo3DDocValuesField.newPathSort. - You expect a 20 km corridor with a round 20 km cap at each end: documents 12 and 16 km north of the last point match, documents 30 and 40 km south of the first point do not.
- You get the reverse at both ends: 12 and 16 km north are missed (sort value +Infinity), and 30 and 40 km south are matched (sort values 30023 and 40034 m). No error.
- Why: each cap is a plane slice through the end segment's two corners, facing the end point's direction from the earth's centre. On the flattened earth that direction is 'straight up' only at a point 21 km nearer the equator, so the cap is centred there: it reaches 8 km past the north end and 50 km past the south end.
- The shift does not depend on the width: a 1 km path matches points 42.8 km past its start. In the tested controls, east-west ends, the corridor sides and spheres are right. All 16 releases run, from 5.3.0 (2015) to 10.5.2, and main are affected; 10.6.0 has the same code (read).
Applications that use lucene-spatial3d path queries or path sorts (Geo3DPoint.newPathQuery, Geo3DDocValuesField.newPathSort, newOutsidePathSort) on WGS84 or another ellipsoid, whenever an end segment runs partly north-south away from the equator. One document is enough; only documents near the path ends are affected. Spheres and east-west ends are fine.
Mechanism, terms and numbers772 words
Geo3DPoint.newPathQuery and Geo3DDocValuesField.newPathSort build a GeoStandardPath: a corridor of the given width along the path, closed at each end by a cap that is round on a sphere. Each end cap is the part of the planet cut off by one plane through the two corners of the end segment, whose normal is constrained to the plane through the corners and the earth's centre. For an end segment that runs north-south that normal is the end point's direction from the centre. On a sphere that is the surface normal, so the cap is centred on the end point; on the WGS84 ellipsoid it is the surface normal of a point about 0.19 degrees (21.4 km at 44-45N) closer to the equator, so the cap circle is centred there while still passing through the corners. With pathWidthMeters 20000 on the path (44N,20E) to (45N,20E), documents 12 and 16 km north of the last point are missed (newPathSort +Infinity) and documents 30 and 40 km south of the first point are matched (30023 and 40034 m). The caps reach 7.8 km past the poleward end and 50.5 km past the equatorward end; a 1 km path reaches 42.8 km past its start. Nothing throws. All 16 releases run, from 5.3.0 (the first with WGS84) to 10.5.2, and main are affected; the releases in between and 10.6.0 have the same construction (read).
- geo3d path (
lucene-spatial3d): aGeoStandardPath, built byGeo3DPoint.newPathQuery,Geo3DDocValuesField.newPathSort/newOutsidePathSortandGeoPathFactory.makeGeoPath. It is meant to be a corridor of half-width w along the segments between the points, closed past the first and last points by a cap (on a sphere, a round cap of radius w), with joints at turns.pathWidthMetersis used as w, the distance from the path (Geo3DUtil.java:139). - End cap: geo3d represents it as the part of the planet on one side of one plane, a plane slice, kept only beyond the end segment's cutoff plane. The plane goes through the segment's two corners, so corridor and cap meet.
- WGS84: the ellipsoid model (a = 6378137 m, b = 6356752.314245 m). On it, 'straight up' (the surface normal) and the direction from the earth's centre differ by up to 0.19 degrees, at 45 degrees latitude.
GeoStandardPath.done()makes the first and last pointsCutoffSingleCircleSegmentEndpoints, with the end segment's cutoff plane and two corners (GeoStandardPath.java:192-241).- The cap plane is
SidedPlane.constructSidedPlaneFromTwoPoints(point, upper, lower)(GeoStandardPath.java:997-1007), that isPlane.constructPerpendicularCenterPlaneTwoPoints(Plane.java:250-260): a plane through the two corners, perpendicular to the plane through the corners and the origin. - For a north-south end segment the corners are mirror images east and west of the end point, so the cap plane's normal is the end point's direction from the centre.
- A plane cuts an ellipsoid in an ellipse whose centre lies inside the earth, on the line from the earth's centre to the surface point whose surface normal is parallel to the plane's normal; that point is the cap's apex, and the cap is centred on it. For (45N, 20E) it is at 44.81N: 0.19 degrees, 21.4 km toward the equator (18.6 km at 60N, zero at the equator and the poles). That holds for oblate models such as WGS84 and Clarke 1866; on a prolate model (b > a, which
PlanetModelalso accepts) the same formula moves the apex poleward (not run). - The circle still passes through the corners, w east and west of the end point, so its radius is about sqrt(21.4^2 + w^2) km. Past a poleward end it reaches sqrt(21.4^2 + w^2) - 21.4 km; past an equatorward end 21.4 + sqrt(21.4^2 + w^2) km.
- Path (44N,20E) to (45N,20E), w = 20 km: the north cap reaches 7.8 km, the south cap 50.5 km (the flat sketch above gives 7.9 and 50.7).
- w = 5 km: 0.55 km and 43.3 km; w = 1 km: 0.02 km and 42.75 km; w = 200 km: 0.89 w and 1.11 w.
- The reversed path and the southern mirror (44S to 45S) behave the same at their poleward and equatorward ends; a north-east end segment gives 0.50 w and 2.02 w; at 60N 0.44 w and 2.29 w.
- Controls within about 1% of w, in the tested cases: a sphere (1.00 w), east-west end segments (1.01 w), the corridor sides (0.995 w), one-point paths and interior joints at 90-degree turns (0.99 w). A path from the equator to 1N gives 0.99 w at its equator end and 0.95 w at its 1N end, where the shift is already about 0.7 km.
Exposure and likelihood345 words
- Who is hit. Users of the
lucene-spatial3dpath query, path sort and outside-path sort, or ofGeoPathFactory.makeGeoPath, withPlanetModel.WGS84,CLARKE_1866or another oblate model (a prolate model is mis-centred the other way). Also, by reading, spatial-extrasGeo3dShapeFactorybuffered line strings when the context factory is configured withplanetModelwgs84 or clarke1866 (other spellings,clarke_1866included, throw "Unknown planet model"), or with its publicplanetModelfield set toPlanetModel.WGS84orCLARKE_1866; its default is the sphere, which is not affected. - Default vs opt-in. Opt-in by module and API (
Geo3DPointandGeoPathare@lucene.experimental). Within it the trigger is ordinary: WGS84 is whatGeo3DPoint's 3-argument constructor uses, andnewPathQuery/newPathSorttake thePlanetModelexplicitly, so a caller passes WGS84 to match points indexed that way; any route with a north-south end at mid latitudes then hits it. Index size does not matter. - Effect. Wrong matches and wrong sort values near the path ends: documents within the width past a poleward end are dropped (sort value +Infinity, which marks "points outside of the shape"), documents far past an equatorward end match with finite sort values. For a 20 km path the poleward cap keeps 39% of its straight-out reach and the equatorward cap reaches 2.5 times the width; for narrow paths the equatorward cap reaches about 43 km whatever the width. Nothing throws, nothing is written.
- Where it is small. Near the equator and the poles (on a path from the equator to 1N the reach is 0.99 w at the equator end and already 0.95 w at the 1N end), and, in the tested cases, for east-west end segments, along the corridor sides, at 90-degree interior joints and for one-point paths. The shift is a fixed distance (about 21 km at mid latitudes), so wide paths suffer less: 0.89 w and 1.11 w at 200 km.
- How likely. The module is opt-in and only the path ends are affected; the recorded search found no report since 5.3.0 (2015), and no upstream test checks points past a path end on WGS84.
on WGS84 a 1 km geo3d path (44N,20E)-(45N,20E) from GeoPathFactory.makeGeoPath (the shape behind Geo3DPoint.newPathQuery) contains (43.82N,20E), 20 km south of its first point, because the end cap is shifted toward the equator.
/*
* 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 TestGeoPathEndCapWGS84 extends LuceneTestCase {
public void testOneKmPathDoesNotContainPoint20KmBeyondItsStart() {
PlanetModel pm = PlanetModel.WGS84;
GeoPoint start = new GeoPoint(pm, Math.toRadians(44.0), Math.toRadians(20.0));
GeoPoint end = new GeoPoint(pm, Math.toRadians(45.0), Math.toRadians(20.0));
GeoPath path = GeoPathFactory.makeGeoPath(pm, 1000 / pm.getMeanRadius(), new GeoPoint[] {start, end});
GeoPoint south = new GeoPoint(pm, Math.toRadians(43.82), Math.toRadians(20.0));
// expected: a 1 km wide path does not contain a point 20 km south of its first point; actual: it does
assertFalse(
"(43.82N,20E), 20 km south of the start of the 1 km path (44N,20E)-(45N,20E): expected outside, got isWithin=true",
path.isWithin(south));
}
}
# in a copy of this finding's repro folder (repro/LFV-736/, 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.TestGeoPathEndCapWGS84
- Expected vs actual
- Expected isWithin = false (about 20 times the path width away); actual true on main 645d4fe14a2 with -ea and -da.
More files, environment, full output47 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: (43.82N,20E), 20 km south of the start of the 1 km path (44N,20E)-(45N,20E): expected outside, got isWithin=true
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.assertTrue(Assert.java:42)
org.junit.Assert.assertFalse(Assert.java:65)
org.apache.lucene.spatial3d.geom.TestGeoPathEndCapWGS84.testOneKmPathDoesNotContainPoint20KmBeyondItsStart(TestGeoPathEndCapWGS84.java:30)- The class promises "a specified half-width";
newPathQuerystates no error, whilenewDistanceQueryallows 0.5% on WGS84. - Corridor sides, east-west caps and one-point paths stay within 1% when tested; only end caps of north-south segments move.
- LUCENE-6544: "Circles are now always centered on their center point, no matter how significantly ellipsoid the planet is" (about circles and one-point paths).
The full argument276 words
- Contract.
GeoStandardPath: "GeoShape representing a path across the surface of the globe, with a specified half-width. Path is described by a series of points. Distances are measured from the starting point along the path, and then at right angles to the path." (GeoStandardPath.java:29-32). The end components are "Endpoint that's a simple circle" (:990) and "Endpoint that's a single circle with cutoff(s)" (:1084).newPathQuery: "@param pathWidthMeters width of the path in meters." with no error statement (Geo3DPoint.java:192-213);newDistanceQuery: "if PlanetModel#WGS84 is used, the query is approximate and may have up to 0.5% error" (:112-113). - Size. For a 20 km path 61% of the poleward cap's straight-out reach is lost and the equatorward cap reaches 2.5 times the width; a 1 km path matches documents 42.8 km away.
- History. Before 5.3.0 the cap was
new SidedPlane(point, point, -originDistance), exact on the unit sphere. LUCENE-6487 replaced it with the two-corner plane so the corridor would "line up with the circles at the waypoints"; the cap's position was not discussed. LUCENE-6544 (#7602): "Circles are now always centered on their center point, no matter how significantly ellipsoid the planet is." That re-anchored circles and the one-point path endpoint, not multi-point path ends, and left this end cap as it was. LUCENE-8696's commit: "Rework how endpoint circles are represented to allow for consistency on WGS84". - Tests. No upstream test asserts end-cap extent on WGS84 (
TestGeoPath's WGS84 tests cover bounds, a degenerate-path failure, LUCENE-8696 and identical points); the random query tests compare the query with the shape's ownisWithin, so a wrong shape agrees with itself.
The devil's-advocate check298 words
Strongest case for "intended" (rated strong by the reviews from another model family, OpenAI GPT-5.6, and argued in full by a devil's advocate): geo3d builds shapes from plane slices and is approximate on ellipsoids; GeoStandardCircle says a one-plane circle "is a circle for spherical planets and an ellipse otherwise"; the private SegmentEndpoint javadoc says an endpoint "is not necessarily a circle" and case (2) "must be constructed to go through two supplied points and be perpendicular to a connecting plane"; constructPerpendicularCenterPlaneTwoPoints is "useful for building path endpoints on worlds that can be ellipsoids"; GeoStandardCircle notes "In an ellipsoidal world, cutoff distances make no sense, unfortunately. Only membership can be used to make in/out determination."; the classes are experimental or internal; and the geo3d author wrote in LUCENE-8696 that retrofitting paths to WGS84 "involved compromises".
Why it still fails: the private javadoc does pin this plane (a plane through both corners and perpendicular to the plane through them and the origin is unique), so the code does what it says, and fix A would change that javadoc line. But the javadoc fixes a construction, chosen in LUCENE-6487 so the corridor lines up with the end circles; where the cap then sits on an ellipsoid was not discussed. The class promises a half-width, and the corridor sides and one-point paths keep it. An ellipse is fine; GeoStandardCircle is one on WGS84, but anchored on its centre point. Its "only membership can be used" comment is about distance values; here membership itself is wrong. The "compromises" remark was about a random-test path with a 1.34 rad (77-degree) half-width and about joints. A 1 km path matching documents 42.8 km away is hard to read as an accepted approximation. Verdict: not intended, a defect, with a strong case the other way.
Recommended A: three-point cap plane through the corners and the cap tip
Build each end cap plane through the two corners and the cap tip, one half-width past the end point, with the existing three-point plane helper (+46/-1 in GeoStandardPath.java). In every sampled case each cap then reaches 0.99-1.00 of the width; the new test, all 183 spatial3d tests, the spatial-extras geo3d tests and the module checks pass.
super(planetModel, previous, point,
endCapPlane(planetModel, point, cutoffPlane, topEdgePoint, bottomEdgePoint));
...
// t = unit normal of the cutoff plane, away from the segment; u = point / |point|
final double alpha =
0.5 * (point.arcDistance(topEdgePoint) + point.arcDistance(bottomEdgePoint));
final GeoPoint tip = planetModel.createSurfacePoint(
ux * Math.cos(alpha) + tx * Math.sin(alpha), /* y, z likewise */ ...);
final SidedPlane plane = SidedPlane.constructNormalizedThreePointSidedPlane(
point, topEdgePoint, bottomEdgePoint, tip);
return plane != null
? plane
: SidedPlane.constructSidedPlaneFromTwoPoints(point, topEdgePoint, bottomEdgePoint);All options and trade-offs3 options ยท 739 words
Two options were built and validated in a scratch worktree at a9779ef3c193 with gradle (JDK 25) and with the standalone runner against the finder's and the verifiers' repros. A passes everything; B is smaller but breaks one upstream bounds assertion on an extreme test planet. Either needs a regression test; this page's test is one.
A: three-point cap plane through the corners and the cap tip (validated by the checks) (recommended)
In CutoffSingleCircleSegmentEndpoint, compute the tip (the surface point at the mean corner angle past the end point, along the outward normal of the cutoff plane) and build the cap with SidedPlane.constructNormalizedThreePointSidedPlane, the helper interior joints already use at turns; fall back to today's plane if that helper returns null (nearly collinear points, or the inside point on the plane). The finder's sketch, reworded and tidied.
super(planetModel, previous, point,
endCapPlane(planetModel, point, cutoffPlane, topEdgePoint, bottomEdgePoint));
...
// t = unit normal of the cutoff plane, away from the segment; u = point / |point|
final double alpha =
0.5 * (point.arcDistance(topEdgePoint) + point.arcDistance(bottomEdgePoint));
final GeoPoint tip = planetModel.createSurfacePoint(
ux * Math.cos(alpha) + tx * Math.sin(alpha), /* y, z likewise */ ...);
final SidedPlane plane = SidedPlane.constructNormalizedThreePointSidedPlane(
point, topEdgePoint, bottomEdgePoint, tip);
return plane != null
? plane
: SidedPlane.constructSidedPlaneFromTwoPoints(point, topEdgePoint, bottomEdgePoint);Trade-offs
- A few trig calls per path end at construction; not benchmarked.
- On a sphere the tip lies on the same circle, so the plane is today's up to rounding (the sphere control and probe give 1.00 w).
TestGeoPath.testPathBoundspasses; on its extreme test planet the bounds still contain every sampled path point (0 of 120,359 grid points outside).- The tip is the end point's radial direction rotated by the mean corner angle and projected to the surface, not a point at exactly the half-width along a geodesic: 1.00 w in every sampled case, but no proof for all geometries. The fallback is today's plane. The private
SegmentEndpointjavadoc's case (2) would need rewording.
What the checks validated
- Gradle, scratch worktree at
a9779ef3c193, seed DEADBEEF:TestGeo3DPathEndCaps2/2 with assertions on and off; the whole spatial3d module, 20 classes, 183 tests, 3 skipped, 0 failures (TestGeoPath11/11,TestGeo3DPoint17,TestRandomGeoShapeRelationship6,TestGeo3DDocValues); the spatial-extras geo3d tests, 21 tests, 4 skipped, 0 failures;:lucene:spatial3d:check -x test(google-java-format, ecjLint, forbiddenApis) passes. - Standalone runner, patched class shadowed: the finder's test 3/3, the independent reproducer's 8/8, the devil's advocate's 1/1, this page's 2/2 (assertions on and off); reach 1.00 w at every sampled end and bearing (0.99 at the equator).
Keep the plane through the two corners, but use the surface normal at the end point, (x / a^2, y / a^2, z / b^2), minus its component along the corner chord. +31/-1, no trigonometry; on a sphere it is exactly today's plane.
final double nx = point.x * planetModel.inverseXYScalingSquared; // ny likewise
final double nz = point.z * planetModel.inverseZScalingSquared;
final double dx = topEdgePoint.x - bottomEdgePoint.x; // dy, dz likewise
final double t = (nx * dx + ny * dy + nz * dz) / (dx * dx + dy * dy + dz * dz);
final Vector normal = new Vector(nx - t * dx, ny - t * dy, nz - t * dz).normalize();
if (normal == null) {
return SidedPlane.constructSidedPlaneFromTwoPoints(point, topEdgePoint, bottomEdgePoint);
}
return new SidedPlane(point, normal, -normal.dotProduct(topEdgePoint));Trade-offs
- Breaks one upstream assertion:
TestGeoPath.testPathBounds(TestGeoPath.java:256,assertTrue(area.isWithin(point))). On its extreme planet (a = 0.75, b = 1.25, width 0.75 rad) the test point lies outside the path on stock and with both fixes; with B the bounds no longer include it, but they still contain every sampled path point (0 of 129,751). So the assertion looks like it pins old bounds rather than correctness, but a finite grid does not prove the new bounds complete; adopting B means changing it after a bounds review. - Reach 0.99-1.00 w, the reach of the corridor sides.
What the checks validated
- Standalone, shadowed: this page's test 2/2 (assertions on and off), the finder's 3/3, the independent reproducer's 8/8, the devil's advocate's 1/1, the cross-model reviewer's over-inclusion probe passes.
- Gradle:
TestGeo3DPathEndCaps2/2; spatial3d module 183 tests, 1 failure (testPathBounds); spatial-extras andchecknot run for B.
State that path end caps are approximate on ellipsoids.
Trade-offs
In our view a bound that lets a 1 km path match documents 42.8 km away is not useful.
An application that uses a geo3d path query as a route geofence, for example for eligibility along a route, admits documents tens of km past an equatorward end and drops some within the width of a poleward end.
Semi-formal: a bounded-exhaustive harness recomputed geo3d path membership with an independent 50-digit reference and flagged 114 points robustly inside a WGS84 end cap that the path called outside, all at the poleward end. A sweep over latitude, heading and width showed the pattern; reading the cap construction gave the cause.
The method in full117 words
Method: bounded-exhaustive translation validation (the semi-formal tier), in a hunt unit on geo3d distance functions against a reference.
- A harness recomputed the documented path decomposition (corridor, end caps, joints) with an independent 50-digit BigDecimal reference and compared
GeoStandardPath.computeDistancewith it. Check "inside membership" flagged 114 points per distance style that the reference puts robustly inside an end cap of a WGS84 path but for which the path returned +Infinity, all at the poleward end vertex. - A targeted sweep over latitude, heading and width showed the pattern; reading the cap construction gave the cause, and a fix sketch removed it.
- Verifiers then corrected the first write-up: the equatorward cap over-includes, so it is a shift, not a truncation.
No prior report: 284 recorded queries on 2026-10-27 over GitHub issues, PRs and commits, the files of all 410 open PRs, JIRA, the mailing lists and the web. Closest: #9019 (circle accuracy on WGS84) and #9742 (interior joints), different defects.
Searches5 searches
None of the 24 hits read is this bug. The closest items are below.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene) | 2026-10-27 | identifier queries (GeoStandardPath, CutoffSingleCircleSegmentEndpoint, constructPerpendicularCenterPlaneTwoPoints, newPathQuery, newPathSort) and symptom queries ("GeoPath WGS84", "path endpoint ellipsoid", ...) | no report; related: #9019, #7546, #9742, #7602, #11965, #13056 |
| GitHub, manual | 2026-10-27 | 36 searches over apache/lucene, apache/lucene-solr, apache/solr, Elasticsearch, OpenSearch and all of GitHub | nothing matching |
| Open PRs | 2026-10-27 | changed files of all 410 open apache/lucene PRs | none touches the fix site (#16911's file list exceeded the API cap; its title makes it irrelevant) |
| JIRA and mailing lists | 2026-10-27 | 5 JIRA queries; dev@, java-user@, issues@, commits@, solr-user@ (12 direct searches) | notification traffic of the issues above only |
| Web | 2026-10-27 | 6 searches | unrelated |
Introduced in 5.3.0 by LUCENE-6487 (560f3a1a010, 2015), which added WGS84 and replaced the sphere-only cap with the two-corner plane. Identical numbers on 16 releases from 5.3.0 to 10.5.2 and on main; never fixed.
Versions tested6 rows
Introduced by 560f3a1a010 (2015-05-27, "LUCENE-6487: Geo3D with WGS84 in-progress with David's mods"; JIRA LUCENE-6487, now #7546; svn era, no PR), merged as 77de0ed1cf8 (trunk) and 8c3e6b0ed32 (branch_5x) on 2015-06-02. LUCENE-6544, LUCENE-6632 and LUCENE-8696 left the end caps alone; the 2022 refactor (fc7ce768514, 9.5.0, #11965) re-expressed the same plane with constructSidedPlaneFromTwoPoints. Unchanged on upstream main and branch_10x, 10_6 and 10_5.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | this page's test fails (assertions on and off); reach 0.39 w / 2.53 w |
| 10.5.2, 9.12.3 | affected | query and sort JUnits fail with the same numbers |
| 5.3.0, 5.5.5, 6.0.0, 6.0.1, 6.1.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.4.2, 9.5.0, 10.0.0 | affected | geometry check: identical numbers to two decimals (15 km path at 50N reaches 0.31 w north; 1 km path at 45N reaches 42.81 km south) |
| 5.2.1 | sphere only | no WGS84 model; caps exact |
| fix A on main | not affected | all tests pass; reach 0.99-1.00 w |
| fix B on main | 1 upstream assertion | repros pass, reach 0.99-1.00 w; TestGeoPath.testPathBounds fails |
Reproduced by the finder, an independent reproducer, two executed reviews by another model family, a devil's advocate, the adjudication, the follow-up re-runs and a credibility audit. All rated it real; the strongest case for intended behaviour (the private javadoc prescribes this plane) was rated strong but outweighed. They corrected the first write-up from a truncation to a shift.
Checks and verdicts371 words
Each check ran its own tests on main a9779ef3c193 (the cross-model reviews on ea57360912ed, same spatial3d code).
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate, corrected later | Harness: 114 points inside a WGS84 end cap scored outside; JUnit fails 2 of 3 on main, 10.5.2 and 9.12.3; claimed only a poleward truncation. |
| refute review (OpenAI GPT-5.6, executed) | real, high | Own exact-shape, query and grid tests fail on main; root cause agrees; the fix passes 32 upstream tests; intended argument strong but outweighed. |
| Independent reproducer | real | Own test fails 5 of 8 on main, 10.5.2, 9.12.3; found the equatorward over-inclusion (a 1 km path matches points 5-30 km past its start); joints fine. |
| Intended lens (OpenAI GPT-5.6, executed) | real, high | A point 11.1 km past the end of a 20 km path is outside; 13 points 25 km or more past the start are inside; none with the fix. |
| Devil's advocate | real, high | Measured the reach with Vincenty distances (7.8 / 50.5 km at 20 km); own 1 km JUnit fails on main, 10.5.2, 9.12.3; rebutted every intended reading. |
| adjudication | confirmed, high | Re-ran the finder's and the independent reproduction's tests with assertions on and off; retitled the bug as a shift; security none; not a duplicate. |
| Follow-up and audit | keep | Re-ran the finder's and the intended-behaviour check's tests on main and with the fix; history on 16 releases; prior-art search new. |
| this page | reproduced; fix A validated | New test fails on main and 10.5.2, gradle and standalone; fix A passes the spatial3d module, spatial-extras geo3d tests and checks. |
| fact check (executed) | holds after 3 text fixes | Re-ran this page's test with other seeds through gradle and the standalone runner (main and 10.5.2, assertions on and off), fix A's module tests and checks, and the reach probe; found one wrong configuration token, two short line ranges and one mis-grouped control, fixed here. |
| cross-model fact check (OpenAI GPT-5.6) | real; corrections needed | Confirmed the behaviour and the numbers; showed that the private javadoc pins the current plane, that the direction holds for oblate models only, and several overclaims. Rated the intended case strong. Fixed here; see an unpublished file. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 458 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,605 words
- Registry title
- On WGS84, geo3d path end caps are shifted toward the equator, so Geo3DPoint.newPathQuery and Geo3DDocValuesField.newPathSort miss documents just beyond a poleward path end (sort value +Infinity) and match documents tens of km beyond an equatorward end
- Area
- spatial3d / geom path membership (GeoStandardPath end caps; Geo3DPoint.newPathQuery, Geo3DDocValuesField.newPathSort)
- Priority
- Medium, rank 190: Geo3DPoint.newPathQuery and newPathSort on the default WGS84 model silently drop or add documents tens of km from ordinary north-south path ends, a matching error in the little-used spatial3d module.
- Impact class
- silent wrong results
- Impact note
- Applications that run geo3d path queries or path sorts (Geo3DPoint.newPathQuery, Geo3DDocValuesField.newPathSort or newOutsidePathSort, GeoPathFactory.makeGeoPath, and spatial-extras Geo3dShapeFactory buffered line strings) on WGS84, Geo3DPoint's default planet model, or any other non-spherical model are hit with ordinary data at any index size whenever the path's first or last segment has a north-south component away from the equator and the poles: documents within the width just beyond the poleward end are silently dropped (newPathSort gives them +Infinity) and documents up to about 43-50 km beyond the equatorward end are matched with finite sort values (at 44-45 degrees), while SPHERE, east-west ends, interior joints, corridor sides and one-point paths stay within about 1% of the width.
- Summary
- You run Geo3DPoint.newPathQuery (or Geo3DDocValuesField.newPathSort) on WGS84 with a north-south path, for example from (44N,20E) to (45N,20E), and expect it to match the points within pathWidthMeters of the path, with round end caps of that radius around the first and last points, as it does on PlanetModel.SPHERE. Instead both end caps are shifted about 21 km toward the equator: with pathWidthMeters 20000 the query misses documents 12 and 16 km beyond the north end (sort value +Infinity), and with pathWidthMeters 1000 it matches documents 10 to 40 km south of the first point (sort values 10006 to 40034 m). The end-cap plane (GeoStandardPath.CutoffSingleCircleSegmentEndpoint, built by CircleSegmentEndpoint through the two segment corners with its normal inside the origin-containing cutoff plane) points along the end point's geocentric direction, which on an ellipsoid is not the surface normal there, so the cap circle is centred equatorward of the end point.
- Introduced
- 560f3a1a010407f9fd8ed1d84636464891248172 (2015-05-27, lucene6487 branch, 'LUCENE-6487: Geo3D with WGS84 in-progress with David's mods'; JIRA LUCENE-6487, now apache/lucene#7546; patch by DaddyWri (Karl Wright), committed by dsmiley (David Smiley); svn era, no pull request), merged to trunk as 77de0ed1cf8 and to branch_5x as 8c3e6b0ed32 (2015-06-02). In GeoPath.SegmentEndpoint case (2) (path start and end) it replaced the sphere-only cap new SidedPlane(point, point, -originDistance) (normal = the end point's direction, exact on the unit sphere) with SidedPlane.constructNormalizedPerpendicularSidedPlane(point, cutoffPlane, topEdgePoint, bottomEdgePoint), added in the same commit (plane through the two corners, normal inside the cutoff plane). Later changes kept the geometry: LUCENE-6544/6632 (2015) and LUCENE-8696 (2019, interior joints only) did not touch end caps, and the 2022 hierarchical refactor (fc7ce768514 on main, 83a81262b85 on 9.x, first in 9.5.0; #11965) re-expressed it as SidedPlane.constructSidedPlaneFromTwoPoints, the same plane. [read: git log -S and git show in the history clone; exec: 5.2.1 is sphere-only and passes, 5.3.0 fails, 9.4.2 and 9.5.0 give identical numbers] (2015-05-27 (branch commit); merged 2015-06-02 (trunk and branch_5x))
- First affected version
- 5.3.0 [exec], the first release with the WGS84 planet model (CHANGES: 'LUCENE-6487: Spatial Geo3D API now has a WGS84 ellipsoid world model option'); 5.2.1, the last release before, is unit-sphere only and cannot show the bug [exec: its end caps are exact, and a PlanetModel compile probe fails with 'cannot find symbol']
- Affected versions
- every release from 5.3.0 through 10.5.2 (the last release before 10.6.0), 10.6.0 (by reading) and main a9779ef3c193; never fixed. Executed with a version-portable geometry main() (WGS84 path 50.0N->50.3N at 10E, half-width 15 km: 3 of 3 probes at 0.4/0.6/0.8 w beyond the poleward end missed, north cap reach 0.31 w; path 45.0N->45.2N, half-width 1 km: 4 of 4 points 5/10/20/30 km south of the first point wrongly inside, south reach 42.81 km; SPHERE control correct) on 5.3.0, 5.5.5, 6.0.0, 6.0.1, 6.1.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.4.2, 9.5.0, 9.12.3, 10.0.0, 10.5.2 and main, with identical numbers to two decimals in every one. End-to-end JUnits (newPathQuery, newPathSort) fail on 9.12.3, 10.5.2 and main (-ea and -da on main). Other 5.3-10.5 minor releases are inferred from the unchanged construction [read]. Upstream main a95c754ee003, branch_10x, branch_10_6 and branch_10_5 carry the same code at the fix site [read]. Not affected: 5.2.1 and older (sphere only). All runs on JDK 25.0.3.
- 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; the prior-report record, verdict new: 284 recorded queries, namely scripted prior-report searches A (identifiers) and B (symptoms and usage) plus the finder's and the independent reproduction/intended-behaviour check runs over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists and the JIRA archive; 36 manual GitHub searches (apache/lucene, apache/lucene-solr, apache/solr, Elasticsearch, OpenSearch, all of GitHub); the changed files of all 410 open apache/lucene PRs; 5 JIRA REST queries; 12 direct list searches including solr-user@; 6 web searches; the upstream history of the 3 fix-site files on 5 branches; 24 hits read, none the same bug). Related, not reports: apache/lucene#9019 (LUCENE-7970, closed), GeoStandardCircle edge errors on WGS84 (a centred one-plane ellipse), which led to GeoExactCircle and mentions path circles once in passing; #7546 (LUCENE-6487), which introduced the two-corner cap so the corridor would 'line up with the circles at the waypoints'; #9742 (LUCENE-8696), interior-joint gaps; #7602 (LUCENE-6544), 'Circles are now always centered on their center point'; #11965, the 2022 refactor that kept the geometry; #13056 (open), a quantisation failure at a path start on the equator, where this shift is zero. Not fixed upstream: GeoStandardPath.java, SidedPlane.java and Plane.java were last changed on main by dfd536186fe (2026-04-01, an instanceof-pattern cleanup), and no open PR touches them. Gaps: some GitHub and web searches were throttled and retried; open PR #16911 ('Decommission branch 10x') exceeded the 3000-file API cap, so its full file list was not verified; list search is keyword-based. Update 2026-11-02: #16911 (open when searched) merged 2026-10-29 as 48cf3f515d6, which emptied branch_10x to a README tombstone (10.6 is the last 10.x minor; 10.x fixes go to branch_10_6); it fixes nothing here.
- 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 release jars (this doc's test, assertions on and off). The hunt ran on maina9779ef3c193, its cross-model reviews onea57360912ed(no spatial3d file differs); the follow-up's geometry check ran on 16 releases from 5.3.0 to 10.5.2 and main, and the query and sort JUnits on 9.12.3 and 10.5.2. Fix A was validated with gradle (the new test, the whole spatial3d module, the spatial-extras geo3d tests and the module's checks) and with the standalone runner against the finder's and verifiers' repros; fix B the same way, with one upstream assertion failing.
- Finding record
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoStandardPath.java:29-35 (at a9779ef3c193) (class javadoc: 'a path across the surface of the globe, with a specified half-width'; @lucene.internal), :196-238 (done(): the first and last endpoints are CutoffSingleCircleSegmentEndpoint built with the segment corners ULHC/LLHC and URHC/LRHC), :815-827 (SegmentEndpoint javadoc: 'not necessarily a circle', case (2) 'The circle plane must be constructed to go through two supplied points and be perpendicular to a connecting plane'), :991-1007 ('Endpoint that's a simple circle'; CircleSegmentEndpoint(planetModel, previous, point, upperPoint, lowerPoint): circlePlane = SidedPlane.constructSidedPlaneFromTwoPoints(point, upperPoint, lowerPoint)), :1107-1119 (CutoffSingleCircleSegmentEndpoint constructor)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/SidedPlane.java:228-236 (at a9779ef3c193) (constructSidedPlaneFromTwoPoints calls Plane.constructPerpendicularCenterPlaneTwoPoints)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/Plane.java:250-260 (at a9779ef3c193) (constructPerpendicularCenterPlaneTwoPoints: plane through the two points, perpendicular to the plane through them and the origin; 'useful for building path endpoints on worlds that can be ellipsoids')lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPathFactory.java:38-44 (at a9779ef3c193) (makeGeoPath returns GeoStandardPath for widths at or above Vector.MINIMUM_ANGULAR_RESOLUTION) and geom/GeoPath.java:22 (@lucene.experimental)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java:42 (at a9779ef3c193) (@lucene.experimental), :65 (default planet model WGS84), :112-113 (newDistanceQuery: 'if PlanetModel#WGS84 is used, the query is approximate and may have up to 0.5% error'), :192-213 (newPathQuery: '@param pathWidthMeters width of the path in meters', no error statement)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DUtil.java:123-141 (at a9779ef3c193) (fromPath: pathWidthMeters / (meanRadius * xyScaling) passed as the cutoff angle to GeoPathFactory.makeGeoPath)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DDocValuesField.java:194-225 (at a9779ef3c193) (newPathSort: 'orders documents by ascending distance along the described path'; documents missing the field get +Infinity) and :365-375 (newOutsidePathSort uses the same Geo3DUtil.fromPath)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDistance.java:46-47 (at a9779ef3c193) (computeDistance: 'A return value of Double.POSITIVE_INFINITY should be returned for points outside of the shape')lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShapeFactory.java:317 (at a9779ef3c193) (buffered line strings are built with GeoPathFactory.makeGeoPath)lucene/CHANGES.txt:10248 (at a9779ef3c193) ('LUCENE-6487: Spatial Geo3D API now has a WGS84 ellipsoid world model option.')
- The finding in 30 seconds
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoStandardPath.java:29-35, :192-241, :990-1007, :1084-1119, :1440-1500 (main a9779ef3c193)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/SidedPlane.java:228-236; lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/Plane.java:250-260lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DUtil.java:123-141WGS84 axes from PlanetModel.java:35; geocentric latitude atan((b/a)^2 tan(lat)), computed for this doc
- Who is affected
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java:42, :64-66, :192-213; lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DDocValuesField.java:194-225, :216-221, :365-375lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDistance.java:45-48; lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPathFactory.java:38-44; lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPath.java:22lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShapeFactory.java:314-318; Geo3dSpatialContextFactory.java:33, :61-71
- Why it looks unintended
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoStandardPath.java:29-32, :815-827, :990, :1084; lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/Plane.java:250-253; lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoStandardCircle.java:25-26, :66-67; lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestGeoPath.java:496-503lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java:112-113, :192-213
- Suggested fix
lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestGeoPath.java:225-256lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoStandardPath.java:1198-1260 (interior joints use constructNormalizedThreePointSidedPlane); lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/SidedPlane.java:250
- History
lucene/CHANGES.txt:10248
- TestGeoPathEndCapWGS84.java (JUnit test):
repro/LFV-736/src/org/apache/lucene/spatial3d/geom/TestGeoPathEndCapWGS84.java(34 lines)
A step-by-step picture made in parallel with this doc from the executed repro and reach-probe numbers. The PNG is the closing summary frame. Step 7 shows the finder's fix sketch, the same logic as Option A below.
assets/LFV-736/bug.png(458 KB)
