All findings · Lucene finding · LFV-736 · geostandardpath-end-cap-shifted-equatorward-on-wgs84

On WGS84, geo3d path end caps are shifted up to about 21 km toward the equator

On WGS84, geo3d path end caps sit up to about 21 km too close to the equator. A 20 km path from (44N,20E) to (45N,20E) misses documents 12 and 16 km past its north end and matches documents 30 and 40 km past its south end.

Summary

Priority
Medium, rank 190Geo3DPoint.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
silent wrong results
Since
5.3.0 (2015), LUCENE-6487, the change that added WGS84introduced 2015-05-27 · over 11 years
Affected
16 releases run from 5.3.0 to 10.5.2, and main, all affected (releases in between and 10.6.0 have the same code, read)
Status
documented
Found by
bounded-exhaustive check against a 50-digit reference (semi-formal)

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

The finding in 30 seconds

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.

Diagram of the bug: a north-south geo3d path on WGS84 and its two end caps, which sit about 21 km toward the equator, so the north cap falls short of the path width and the south cap overshoots it.
Where the end caps of a north-south path should be and where WGS84 puts them: about 21 km toward the equator, so the north cap falls short and the south cap overshoots. Select the still to open it at full size. Generated with AI, checked against the code.
  1. You index points with Geo3DPoint on WGS84 and search the path (44N,20E) to (45N,20E) with Geo3DPoint.newPathQuery(..., 20000, WGS84), sorting with Geo3DDocValuesField.newPathSort.
  2. 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.
  3. 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.
  4. 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.
  5. 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).

Who is affected

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).

Terms

  • geo3d path (lucene-spatial3d): a GeoStandardPath, built by Geo3DPoint.newPathQuery, Geo3DDocValuesField.newPathSort / newOutsidePathSort and GeoPathFactory.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. pathWidthMeters is 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.

The bug

  1. GeoStandardPath.done() makes the first and last points CutoffSingleCircleSegmentEndpoints, with the end segment's cutoff plane and two corners (GeoStandardPath.java:192-241).
  2. The cap plane is SidedPlane.constructSidedPlaneFromTwoPoints(point, upper, lower) (GeoStandardPath.java:997-1007), that is Plane.constructPerpendicularCenterPlaneTwoPoints (Plane.java:250-260): a plane through the two corners, perpendicular to the plane through the corners and the origin.
  3. 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.
  4. 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 PlanetModel also accepts) the same formula moves the apex poleward (not run).
  5. 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.

The numbers

  • 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-spatial3d path query, path sort and outside-path sort, or of GeoPathFactory.makeGeoPath, with PlanetModel.WGS84, CLARKE_1866 or another oblate model (a prolate model is mis-centred the other way). Also, by reading, spatial-extras Geo3dShapeFactory buffered line strings when the context factory is configured with planetModel wgs84 or clarke1866 (other spellings, clarke_1866 included, throw "Unknown planet model"), or with its public planetModel field set to PlanetModel.WGS84 or CLARKE_1866; its default is the sphere, which is not affected.
  • Default vs opt-in. Opt-in by module and API (Geo3DPoint and GeoPath are @lucene.experimental). Within it the trigger is ordinary: WGS84 is what Geo3DPoint's 3-argument constructor uses, and newPathQuery / newPathSort take the PlanetModel explicitly, 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.

Minimal repro

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.

TestGeoPathEndCapWGS84.java (JUnit test)34 lines
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.lucene.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));
  }
}
Command
# 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.

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

More files, environment, full output47 words

Environment

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

Observed output

# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
    => java.lang.AssertionError: (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)

Why it looks unintended

  • The class promises "a specified half-width"; newPathQuery states no error, while newDistanceQuery allows 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 own isWithin, 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.

Suggested fix

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.testPathBounds passes; 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 SegmentEndpoint javadoc's case (2) would need rewording.

What the checks validated

  • Gradle, scratch worktree at a9779ef3c193, seed DEADBEEF: TestGeo3DPathEndCaps 2/2 with assertions on and off; the whole spatial3d module, 20 classes, 183 tests, 3 skipped, 0 failures (TestGeoPath 11/11, TestGeo3DPoint 17, TestRandomGeoShapeRelationship 6, 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).

B: take the cap plane's normal from the surface normal at the end point (validated by the checks)

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: TestGeo3DPathEndCaps 2/2; spatial3d module 183 tests, 1 failure (testPathBounds); spatial-extras and check not run for B.

Document an error bound instead (not recommended)

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.

Downstream note

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.

How it was found

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.computeDistance with 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.

Prior reports, history and checks

Prior reports

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.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene)2026-10-27identifier queries (GeoStandardPath, CutoffSingleCircleSegmentEndpoint, constructPerpendicularCenterPlaneTwoPoints, newPathQuery, newPathSort) and symptom queries ("GeoPath WGS84", "path endpoint ellipsoid", ...)no report; related: #9019, #7546, #9742, #7602, #11965, #13056
GitHub, manual2026-10-2736 searches over apache/lucene, apache/lucene-solr, apache/solr, Elasticsearch, OpenSearch and all of GitHubnothing matching
Open PRs2026-10-27changed files of all 410 open apache/lucene PRsnone touches the fix site (#16911's file list exceeded the API cap; its title makes it irrelevant)
JIRA and mailing lists2026-10-275 JIRA queries; dev@, java-user@, issues@, commits@, solr-user@ (12 direct searches)notification traffic of the issues above only
Web2026-10-276 searchesunrelated

History

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.

Versions and branches executed, with results
Version or commitVerdictResult
main a9779ef3c193 affectedthis page's test fails (assertions on and off); reach 0.39 w / 2.53 w
10.5.2, 9.12.3 affectedquery 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 affectedgeometry 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 onlyno WGS84 model; caps exact
fix A on main not affectedall tests pass; reach 0.99-1.00 w
fix B on main 1 upstream assertionrepros pass, reach 0.99-1.00 w; TestGeoPath.testPathBounds fails

Independent checks

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).

Each independent check and its verdict
CheckVerdictWhat it found
finder candidate, corrected laterHarness: 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, highOwn 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 realOwn 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, highA 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, highMeasured 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, highRe-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 keepRe-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 validatedNew 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 fixesRe-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 neededConfirmed 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.

Diagram file

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

Sources and provenance

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

Finding record

LFV-736, snapshot of 2026-11-06

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 main a9779ef3c193, its cross-model reviews on ea57360912ed (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.

Lucene and upstream sources

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-260
  • lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DUtil.java:123-141
  • WGS84 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-375
  • lucene/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:22
  • lucene/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-503
  • lucene/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-256
  • lucene/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

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

Repro files

About the diagrams

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.

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