geo3d distance methods take a distance style, and the squared styles must return the square of the plain distance. On a zero-width path one method forgets to square at the end and another squares twice.
lucene/spatial3d(geo3d) models shapes on a sphere or ellipsoid. AGeoPathis a line through points with a width;GeoPathFactory.makeGeoPathpicks a special zero-width class when the width is (almost) 0. Zero-width paths exist since 7.1.0.- Distance methods take a
DistanceStyle:ARC(angle),LINEAR(straight-line chord),NORMAL, and the squared variantsLINEAR_SQUAREDandNORMAL_SQUARED, the squares ofLINEARandNORMAL. - A path's distance is combined from its pieces (segments and end points). Each style has an "aggregation form" for that: the value itself for most styles, the square root for the squared ones. The rule: convert each piece in, combine, convert back once.
LINEAR_SQUARED should return (about 1e-4) against what the two methods return (about 1e-2, not squared, and 1e-8, squared twice). Select the still to open it at full size. Generated with AI, checked against the code.- Build a path with
GeoPathFactory.makeGeoPath(PlanetModel.SPHERE, 0, points)from (0, 0) to (0, 0.1). A width of 0 (anything below about 3e-12 rad) gives the zero-width path class; a width of 1e-6 gives the regular one. - Take the point (lat 0.01, lon 0.05), 0.01 rad beside the middle of the path. Its
LINEAR(straight-line) distance is 0.0099999583, soLINEAR_SQUAREDshould give 0.000099999 (9.9999e-5). The width-1e-6 path does. - Instead the zero-width path returns 0.0099999583 from
computePathCenterDistance(LINEAR_SQUARED, q), the unsquared value, and 9.9998e-9 fromcomputeOutsideDistance(LINEAR_SQUARED, q), the fourth power.NORMAL_SQUAREDis wrong the same way;ARC,LINEARandNORMALare right. - Why: a squared style combines a path's pieces as square roots and must square once at the end. Since 9.5.0 the pieces return square roots, but
computePathCenterDistancenever squares back, and one piece of the outside distance still returns the plain square, which is then squared again. - No error is raised. Asking "which path is closest?" under
LINEAR_SQUAREDcan pick a wider path 0.0408 rad away over a zero-width path 0.0100 rad away (0.0017 vs 0.0100). Nothing in Lucene uses the squared styles.
Only code that calls GeoPath.computePathCenterDistance or computeOutsideDistance itself with LINEAR_SQUARED or NORMAL_SQUARED on a zero-width path; for it the values are wrong, silently, at every point off the path (outside distance: every such point closer than a chord of 1). Lucene's own queries and distance sorts use ARC and are unaffected. The geom API is @lucene.experimental.
Mechanism, terms and numbers847 words
GeoPathFactory.makeGeoPath turns a path whose width is below Vector.MINIMUM_ANGULAR_RESOLUTION (about 3e-12 rad, for example 0) into a GeoDegeneratePath. For the squared distance styles LINEAR_SQUARED and NORMAL_SQUARED, which combine partial results in an "aggregation form" (the square root) and must convert back once (square) for the final result, this class gets two of its distance methods wrong: computePathCenterDistance returns the plain distance d instead of d^2 at every point off the path, because it never converts back, and computeOutsideDistance returns d^4 at every point off the path closer than a chord of 1 (the planet radius), because one of its pieces returns the plain d^2 into a minimum of aggregation-form values that is then squared. For a point 0.01 rad beside a sphere path that is 0.0099999583 and 9.9998e-9 where 9.9999e-5 is expected; the same path with width 1e-6 returns 9.9999e-5.
The cause is an incomplete edit in commit 839dfb5a2dc (2022-11-24, part of the #11965 spatial3d refactor; branch_9x cherry-pick f69fb6ed99b), first released in 9.5.0: it converted the pieces, not the two places that combine them. 9.4.2 returns d^2. Affected: 9.5.0 to 10.6.0 and main; not fixed upstream. ARC, LINEAR and NORMAL are unaffected, and no Lucene query, sort or comparator uses the squared styles, so the impact is limited to code that calls these GeoPath methods itself. Fix: two lines (validated with the whole lucene/spatial3d test suite).
The mechanism (main 645d4fe14a20, files in lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/)
- The factory picks the class from the width alone.
makeGeoPathreturnsnew GeoDegeneratePath(...)whenmaxCutoffAngle < Vector.MINIMUM_ANGULAR_RESOLUTION(GeoPathFactory.java:40-41), else aGeoStandardPath(:43).GeoDegeneratePathis package-private and@lucene.internal(GeoDegeneratePath.java:33-35); callers only see aGeoPath. - The squared styles' aggregation form is the square root.
toAggregationFormreturnsMath.sqrt(distance)andfromAggregationFormsquares (LinearSquaredDistance.java:63-71,NormalSquaredDistance.java:63-71). The defaults are the identity (DistanceStyle.java:116-118,:146-148), so forARC,LINEARandNORMALa missing conversion changes nothing. - Path-center distance: converted in, never converted out.
computePathCenterDistance(GeoDegeneratePath.java:160-180) takes the minimum ofPathSegment.pathCenterDistanceandSegmentEndpoint.pathCenterDistance, which both returntoAggregationForm(...)(:739,:522), and ends withreturn closestDistance;(:179). For a squared style that is sqrt(d^2) = d. - Outside distance: one piece left plain, then converted out.
outsideDistance(:270-287) takes the minimum ofSegmentEndpoint.outsideDistance(toAggregationForm(...),:534-537) andPathSegment.outsideDistance, which returns the plain valueMath.min(Math.min(startDistance, endDistance), distance)(:866-874,:873), i.e. d^2, and then appliesfromAggregationForm(:286). ForLINEAR_SQUAREDthe result is min(e, d^2)^2, with d the trueLINEARdistance and e the chord to the nearest path vertex. Below d = 1 (geo3d units: the sphere's radius is 1) that is always (d^2)^2 = d^4, beyond the path end too. Above 1 it is d^2 when the nearest path point is a vertex, d^4 while d^2 < e, and e^2, too large, otherwise (executed on the sphere: 1.6084 for d^2 = 1.2753 beside a 2-rad segment).NORMAL_SQUAREDnever exceeds 1 on the sphere, so it gets NORMAL^4 at every point off the path.computeOutsideDistancereaches it throughGeoBaseMembershipShape.java:46-53(0 for points on the path). - The width > 0 sibling does these two methods right.
GeoStandardPathreturnsfromAggregationForm(...)from both (GeoStandardPath.java:278-285,:326-333). ItscomputeDistancehas a separate defect (LFV-271). - A third plain return, masked. The pole branch of
PathSegment.pathCenterDistancereturnscomputeDistance(start, ...)withouttoAggregationForm(GeoDegeneratePath.java:712-714; same atGeoStandardPath.java:1592-1594). In the one pole probe run, the end points win the minimum, so the result is right once the two lines are fixed. - Scope: only these two methods. On the same zero-width path,
computeDistance(a point on the path) andcomputeNearestDistance(a point on and a point off the path) return the square withLINEAR_SQUAREDandNORMAL_SQUARED(executed), and points on the path get 0 from the two affected methods, which is right. At the pole of the path's great circle (the north pole, an equatorial path),computeOutsideDistancethrows aNullPointerExceptionfromPlane.linearDistanceorPlane.arcDistancewithARC,LINEARandLINEAR_SQUAREDalike, on 9.4.2 too: a separate behaviour, not part of this bug.
| input (sphere unless noted) | expected | actual |
|---|---|---|
path (0, 0)-(0, 0.1), width 0, point (0.01, 0.05): computePathCenterDistance(LINEAR_SQUARED) | 9.999916666944443E-5 | 0.009999958333385416 (= LINEAR) |
same: computeOutsideDistance(LINEAR_SQUARED) | 9.999916666944447E-5 | 9.999833334583335E-9 (= LINEAR^4) |
same path with width 1e-6 (GeoStandardPath), path-center | 9.9999e-5 | correct |
| 3-point path lon 0 -> 0.2 -> 0.4, point beyond the end (0, 0.45): outside | 0.002499479210067507 | 6.2473963215596976E-6 (= LINEAR^4) |
| WGS84 path (an independent reviewer's test): center / outside | 1.7910505946914878E-4 / 1.7910505946914875E-4 | 0.013383013841028066 / 3.207862232744731E-8 |
| path (0, 0)-(0, 2.0), point (1.2, 1.0), d = 1.1293 (above 1): outside | 1.275284491046653 | 1.608434539414104 (= e^2, the squared chord to the nearest vertex) |
zero-width path (ARC 0.0100) vs a width-1e-6 path (ARC 0.0408), path-center under LINEAR_SQUARED | zero-width path nearer | wider path reported nearer (0.0099999583 vs 0.0016611674) |
GeoCircle vs the zero-width path, outside LINEAR_SQUARED | both d^2 | circle 8.4094e-4 = 0.0289990^2; path 9.9998e-9 for LINEAR 0.0100 |
Lucene 9.4.2 returns the squares for the zero-width path; every release from 9.5.0 returns the values above.
Exposure and likelihood274 words
- Silent, wide. With a squared style on a zero-width path,
computePathCenterDistanceis wrong at every point off the path, andcomputeOutsideDistanceat every point off the path closer than a chord of 1 (withNORMAL_SQUAREDon the sphere, at every point off the path) and at many farther ones. No exception, no special input; points on the path get 0, which is right. - Who is exposed. Code that calls
computePathCenterDistanceorcomputeOutsideDistancewithLINEAR_SQUAREDorNORMAL_SQUAREDon a path whose width is belowVector.MINIMUM_ANGULAR_RESOLUTION(for example 0). Both choices are opt-in, on the@lucene.experimentalgeom API (GeoPath,GeoPathFactory,DistanceStyle,GeoOutsideDistance). - Lucene's own results are not affected.
LINEAR_SQUAREDandNORMAL_SQUAREDappear nowhere inlucene/outsideDistanceStyle.java; nothing in Lucene callscomputePathCenterDistance; the in-tree distance sorts, including the outside-path sort ofGeo3DDocValuesField, passDistanceStyle.ARC. - Mixed widths can rank wrongly. The
GeoPathjavadoc says the path-center distance "is meant to allow comparisons between different paths to find the one that goes closest to a point". The class follows from the width, so a zero-width path compared with a wider path under a squared style can get the order wrong: it flips when the zero-width path's unsquared d is larger than the other path's squared value (executed; with the same two paths at q = (0.001, 0.1) the order is still right). Outside distances of a zero-width path cannot be compared with those of aGeoCircleeither (d^4 against d^2). - Likelihood: plausible. Zero-width paths are a documented, tested feature, but the squared styles have no in-tree caller and no report of the bug was found (see the prior reports).
One JUnit test in lucene/spatial3d, public API only: a two-point zero-width path on the sphere and one point beside it. It asserts that each LINEAR_SQUARED result is the square of the LINEAR result, one assertion per fix site. No randomness; fails with assertions on and off, no build change needed.
/*
* 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 TestGeoDegeneratePathSquaredStyles extends LuceneTestCase {
public void testZeroWidthPathLinearSquared() {
PlanetModel pm = PlanetModel.SPHERE;
GeoPath path =
GeoPathFactory.makeGeoPath(
pm, 0, new GeoPoint[] {new GeoPoint(pm, 0, 0), new GeoPoint(pm, 0, 0.1)});
GeoPoint q = new GeoPoint(pm, 0.01, 0.05);
// expected LINEAR^2 (~1e-4) from both calls; actual LINEAR (~1e-2), then LINEAR^4 (~1e-8)
double center = path.computePathCenterDistance(DistanceStyle.LINEAR, q);
assertEquals(
"computePathCenterDistance(LINEAR_SQUARED)",
center * center,
path.computePathCenterDistance(DistanceStyle.LINEAR_SQUARED, q),
1e-12);
double outside = path.computeOutsideDistance(DistanceStyle.LINEAR, q);
assertEquals(
"computeOutsideDistance(LINEAR_SQUARED)",
outside * outside,
path.computeOutsideDistance(DistanceStyle.LINEAR_SQUARED, q),
1e-12);
}
}
cd $LUCENE # an apache/lucene checkout at 645d4fe14a20 (or a later commit where GeoDegeneratePath.java is unchanged)
cp $REPRO/TestGeoDegeneratePathSquaredStyles.java lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/
./gradlew :lucene:spatial3d:test --tests org.apache.lucene.spatial3d.geom.TestGeoDegeneratePathSquaredStyles -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false
- Expected
computePathCenterDistance(LINEAR_SQUARED)= 9.999916666944443E-5, the square of theLINEARvalue 0.009999958333385416.- Observed
expected:<9.999916666944443E-5> but was:<0.009999958333385416>on main (assertions on and off) and on the 10.6.0, 10.5.2, 9.12.3 and 9.5.0 jars (assertions on); 9.4.2 passes.
More files, environment, full output181 words
The test sits in org.apache.lucene.spatial3d.geom only because upstream's TestGeoPath does; it uses public classes and methods only (GeoPathFactory, GeoPath, DistanceStyle, PlanetModel, GeoPoint). Nothing is randomized, so the result does not depend on the seed (it fails with DEADBEEF and 5EED0741).
On stock code it stops at the first assertion (path-center). The second assertion is the outside-distance fix site: with only the path-center line fixed it fails with expected:<9.999916666944447E-5> but was:<9.999833334583335E-9>, the same value the stock code returns. NORMAL_SQUARED goes through the same code and is left out.
The same file fails unchanged on the 10.6.0, 10.5.2, 9.12.3 and 9.5.0 release jars (run with assertions on) and passes on 9.4.2.
| lucene | main 645d4fe14a20: affected; also the 10.6.0, 10.5.2, 9.12.3 and 9.5.0 release jars (fail) and 9.4.2 (passes) with this test |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
TestGeoDegeneratePathSquaredStyles > testZeroWidthPathLinearSquared FAILED
java.lang.AssertionError: computePathCenterDistance(LINEAR_SQUARED) expected:<9.999916666944443E-5> but was:<0.009999958333385416>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:37E69C1AE26C7456]:0)
at junit@4.13.2/org.junit.Assert.fail(Assert.java:89)
at junit@4.13.2/org.junit.Assert.failNotEquals(Assert.java:835)
at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:555)
at org.apache.lucene.spatial3d.geom.TestGeoDegeneratePathSquaredStyles.testZeroWidthPathLinearSquared(TestGeoDegeneratePathSquaredStyles.java:31)
# with only the path-center line fixed, the second assertion:
java.lang.AssertionError: computeOutsideDistance(LINEAR_SQUARED) expected:<9.999916666944447E-5> but was:<9.999833334583335E-9>DistanceStyle: convert in, combine, convert back once "to yield a final result". One method never converts back; the other converts back a value never converted in.- The width > 0 class converts back in both methods, and 9.4.2 returned the square on zero-width paths too.
- LUCENE-7942 added aggregation forms because squared path distances were not "true squared distances".
The full argument240 words
- The contract is one conversion back for a final result.
DistanceStyle.java:107-148: "Distances should be converted to aggregation form before aggregation is attempted, and they should be converted back from aggregation form to yield a final result." The squared styles are the "Linear distance squared" and "Normal distance squared" calculators (:36-38,:44-46). - Why aggregation forms exist. #8991 (LUCENE-7942, 2017, by the geo3d author): "The xxxSquaredDistance computations, for example, do not produce true squared distances but rather a distance metric that is a combination of both squared and linear." Its fix added the three conversion methods (
lucene/CHANGES.txt:7864-7865). - The same refactor converts back once in the sibling class.
GeoStandardPathreturnsfromAggregationForm(...)from both methods (:278-285,:326-333);c9c27c755a4("Make sure use of aggregation form is consistent throughout ...") put those two conversions in a day before the introducing commit. (The same commit left a double conversion behindGeoStandardPath.computeDistance, LFV-271, so "consistent" holds for these two methods only.) - 9.4.2 returned d^2: every piece returned the plain value, so the minimum was already the answer. The introducing commit converted the pieces but not the two places that combine them.
- No test pins the current values. No spatial3d test uses a squared style, nothing calls
computePathCenterDistance, andcomputeOutsideDistanceis tested only on boxes, circles and polygons, never on a path. The tests the commit changed compare paths withARC, which cannot see the bug.
The devil's-advocate check169 words
The strongest case for "intended" (the intended-behaviour check and the cross-model reviews): the geom API is @lucene.experimental and GeoDegeneratePath @lucene.internal; the path-center distance is only "meant to allow comparisons", and since sqrt is monotone, zero-width paths still rank correctly among themselves; the squared styles are second-class (they throw "Reverse mapping not implemented" from findDistancePoints and friends); and the aggregation rule speaks of sums, not minima.
Why it fails: experimental and internal allow API changes, not wrong values from the public GeoPath interface; the comparison the javadoc names is "between different paths", and a zero-width and a wider path swap order under LINEAR_SQUARED (executed); computeOutsideDistance has no comparison-only wording and returns d^4, which across the chord-1 crossover is not even one monotone function of d; the same author's design takes minima in aggregation form and converts back once (GeoStandardPath); and 9.4.2 returned the squares. Verdict: not intended, a defect (the independent reproduction; the refute-lens and intended-lens cross-model reviews; the intended-behaviour check; and the adjudication, with high confidence).
Recommended A: convert back once, and return the segment's outside distance in aggregation form
Two lines in GeoDegeneratePath: return distanceStyle.fromAggregationForm(closestDistance) from computePathCenterDistance, and wrap PathSegment.outsideDistance's result in toAggregationForm, as the sibling GeoStandardPath and the end-point code do. The repro passes, the lucene/spatial3d tests give 182 run, 0 failures (3 skipped), and the module's lint checks pass.
@@ computePathCenterDistance
- return closestDistance;
+ return distanceStyle.fromAggregationForm(closestDistance);
@@ PathSegment.outsideDistance
- return Math.min(Math.min(startDistance, endDistance), distance);
+ return distanceStyle.toAggregationForm(
+ Math.min(Math.min(startDistance, endDistance), distance));All options and trade-offs3 options ยท 441 words
Two options were built and validated through gradle on 645d4fe14a20; a third is listed, not built. Workaround until fixed: call the method with LINEAR and square the result. NORMAL works the same way on the sphere, but has its own defect on ellipsoids such as WGS84 (LFV-740), so prefer LINEAR.
A: convert back once, and return the segment's outside distance in aggregation form (validated by the checks) (recommended)
2 lines in GeoDegeneratePath.java (:179, :873), the pattern of GeoStandardPath and of SegmentEndpoint.outsideDistance. ARC, LINEAR and NORMAL are unchanged (identity conversions).
@@ computePathCenterDistance
- return closestDistance;
+ return distanceStyle.fromAggregationForm(closestDistance);
@@ PathSegment.outsideDistance
- return Math.min(Math.min(startDistance, endDistance), distance);
+ return distanceStyle.toAggregationForm(
+ Math.min(Math.min(startDistance, endDistance), distance));Trade-offs
None known; local to the zero-width class. Code that worked around the wrong values (for example by squaring the path-center value itself) would have to drop that; none is known.
What the checks validated
- Gradle, scratch worktree of
645d4fe14a20, seed DEADBEEF: the repro fails on stock and passes with A, assertions on and off. - The whole
:lucene:spatial3d:testwith A and the repro (includingTestGeoPath,TestRandomGeoShapeRelationship,TestGeo3DPoint,TestGeo3DDocValues): 20 suites, 182 tests, 0 failures, 3 skipped. :lucene:spatial3d:check -x test(checkGoogleJavaFormat,ecjLintMain,ecjLintTest,forbiddenApisMain,forbiddenApisTest): passes.- An independent reviewer's 7-test class (WGS84, both squared styles, both methods, a pole probe, two controls): 5 of 7 fail on stock
645d4fe14a20, 7 of 7 pass with A. - Earlier, the same two-line change made every verifier's test pass: the independent reproduction 5 of 5 and the adjudication 12 of 12 on
a9779ef3c193, a separate test 2 of 2 onea57360912ed(lucene/spatial3dis the same on both).
Also return distanceStyle.toAggregationForm(distanceStyle.computeDistance(start, x, y, z)) from the pole branch of PathSegment.pathCenterDistance (:713), so that every piece returns aggregation form (3 lines in total).
Trade-offs
No executed input differs between A and B: in the pole probe the end points win the minimum. The same branch in GeoStandardPath (:1593) belongs to the other class and was not changed here.
What the checks validated
Gradle on 645d4fe14a20: the repro passes; :lucene:spatial3d:test 20 suites, 182 tests, 0 failures, 3 skipped; check -x test passes; the reviewer's class 7 of 7.
Since toAggregationForm is monotone, revert the three wrapped piece returns (:522, :536, :739) and the final fromAggregationForm (:286).
Trade-offs
More lines, and it diverges from the design the same refactor applied to GeoStandardPath; A is smaller.
Lucene's own queries and sorts use ARC and are unaffected; an application that ranks paths by computePathCenterDistance under a squared style can pick the wrong path when zero-width and wider paths are mixed.
A harness recomputed geo3d path distances with an independent 50-digit reference and, on zero-width paths, flagged 2,487 LINEAR_SQUARED and 2,487 NORMAL_SQUARED path-center mismatches on the sphere and 2,554 LINEAR_SQUARED on WGS84, off by up to 1e9 times. Reading the code showed the outside distance is squared twice; a public-API test confirmed both.
The method in full124 words
Method: semi-formal (bounded-exhaustive checking of the real jar against exact semantics).
- A translation-validation harness recomputed the documented path distances with an independent BigDecimal reference (50-digit precision), the squared styles through the aggregation-form contract. On zero-width paths it flagged
computePathCenterDistance: 2,487LINEAR_SQUAREDand 2,487NORMAL_SQUAREDmismatches on the sphere and 2,554LINEAR_SQUAREDon WGS84, off by ratios up to 1e9 (d returned for d^2). - The harness checks outside distances in
ARConly; the outside-distance half was found by reading the code while writing the aggregation-form part of the reference. - The finder, an AI tool, then wrote a public-API JUnit on the stock jar. Independent reproducers wrote their own tests from the claim alone; the one-test repro was cut from the finder's test on 2026-10-29.
No prior report: 215 recorded searches over GitHub issues, PRs and commits, the mailing lists, JIRA, downstream projects and the web, plus the files of all 410 open PRs (2026-10-27), rechecked on 2026-10-31. Closest: LUCENE-7942 (#8991), which defined the conversion rule this breaks.
Searches4 searches
Verdict new: nothing reports squared styles returning the unsquared or the fourth-power distance on a zero-width path, or the missing conversions in GeoDegeneratePath, and no fix exists. Related, not reports of this bug: #8991 (LUCENE-7942, the contract source), #9004 (LUCENE-7955, added GeoDegeneratePath), #9011 (LUCENE-7962, added computePathCenterDistance), #11965 (tracking issue of the 2022 refactor), #11975 (a branch_9x compile fix in the same class that keeps toAggregationForm), and the dev@ replies to the introducing commit (a precommit dead-code failure only). Gaps: GitHub search does not index review comments on PR diffs; closed unmerged PRs were covered by text search only; list search is keyword-based.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits; downstream repositories | 2026-10-27 | the scripted prior-report search in two runs (65 queries each) plus the finder's earlier run; 47 manual searches on apache/lucene, Elasticsearch, OpenSearch, Solr and all of GitHub; the full module:spatial3d label list (134 items); comments since 2022-11-20 on the 175 items found | only the related items above |
| Open PRs | 2026-10-27 | the changed files of all 410 open apache/lucene PRs | none touches GeoDegeneratePath.java, GeoPath/GeoBasePath or the DistanceStyle classes |
| Lists, JIRA, web | 2026-10-27 | 24 direct searches over every lucene.apache.org and solr.apache.org list (the 5 dev@ replies to the refactor read in full); 8 LUCENE and SOLR JIRA queries; 13 web queries | no report |
| Recheck (GitHub) | 2026-10-31 | GeoDegeneratePath; LINEAR_SQUARED; computePathCenterDistance; fromAggregationForm; zero width path distance; the upstream file history (raw responses saved) | GeoDegeneratePath: #11975, #9283, #9004; LINEAR_SQUARED: #7733; computePathCenterDistance, fromAggregationForm: nothing; zero width path distance: 11 unrelated items; none is this bug; the file's last commit is still #15889 (mechanical) |
Introduced by 839dfb5a2dc (2022-11-24, the #11965 spatial3d refactor; branch_9x cherry-pick f69fb6ed99b), first released in 9.5.0. Executed: 9.5.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0 and main fail; 7.1.0 to 9.4.2 return the square. Not fixed upstream.
Versions tested5 rows
Introduced by 839dfb5a2dc on main, "More refactoring work, and fix a distance calculation." (Karl David Wright, 2022-11-24 04:36 UTC), one commit of the #11965 spatial3d refactor series, pushed without a pull request; branch_9x cherry-pick f69fb6ed99b (same patch-id). It wrapped SegmentEndpoint.pathCenterDistance, SegmentEndpoint.outsideDistance and PathSegment.pathCenterDistance in toAggregationForm and the result of outsideDistance in fromAggregationForm, but left computePathCenterDistance's return closestDistance; (2017) and PathSegment.outsideDistance unconverted. Commit-level runs (the geom package of each commit over the 9.4.2 jars): the parents 5e4623af1fd and cab1802308d pass, 839dfb5a2dc and f69fb6ed99b fail. A regression: 9.4.2 (2022-11-21) is the last good release, 9.5.0 (2023-01-30) the first affected. Releases before 7.1.0 have no such API. Upstream main 2f424fd428da (2026-10-30) still has both lines.
| Version or commit | Verdict | Result |
|---|---|---|
| 7.0.1 and older | no such API | zero-width paths, computePathCenterDistance and the aggregation forms all arrived in 7.1.0 |
| 7.1.0 to 9.4.2 | last good: 9.4.2 | returns d^2 (executed at 7.1.0, 7.7.3, 8.11.4, 9.0.0 and 9.4.2; this doc's test passes on 9.4.2) |
| 9.5.0 to 9.12.3 (14 releases) | first: 9.5.0 | this doc's test fails on 9.5.0 and 9.12.3; the other tags carry the same code |
| 10.0.0 to 10.5.2 (12), 10.6.0 | affected | fails on 10.0.0 (follow-up), 10.5.2 and 10.6.0 (this doc's test) |
| main 645d4fe14a20 | affected | this doc's test fails through gradle and the standalone runner, assertions on and off; passes with fix A or B |
An independent reproducer, an intended-behaviour devil's advocate, two executed reviews by another model family (OpenAI GPT-5.6) and an adjudication all rated it real; the adjudication confirmed it with high confidence. The one-test repro still fails on main 645d4fe14a20.
Checks and verdicts242 words
Before the finding was recorded, an executed refute review by OpenAI GPT-5.6 reproduced it with its own WGS84 test (5 of 7 fail, controls pass) and rated the intended-behaviour argument weak.
| Check | Verdict | What it found |
|---|---|---|
| Finder | candidate | Reference harness: 2,487 (sphere) and 2,554 (WGS84) LINEAR_SQUARED path-center mismatches on zero-width paths. JUnit fails 2 of 2 on main (assertions on and off), 10.5.2 and 9.12.3; passes with the fix. |
| Independent reproducer | real | Own test with an analytic oracle: 4 of 5 fail on main (assertions on and off), 10.5.2, 9.12.3 and 9.5.0; 5 of 5 pass on 9.4.2 and with the fix. Found the beyond-the-end case, the chord-1 crossover and the cross-path misranking. |
| Intended lens (OpenAI GPT-5.6, executed) | real (high) | Own contract test: 2 of 2 fail on main and 9.5.0, pass on 9.4.2 and with the fix. Intended-behaviour argument: weak. |
| Devil's advocate | real (high) | Built the strongest intended case and rejected it; its probe shows the cross-path swap and a GeoCircle returning d^2 where the path returns d^4. |
| Adjudication | confirmed (high) | Re-ran the independent reproduction's, the finder's and its own tests: 12 run, 9 fail (assertions on and off), 3 controls pass; 12 of 12 pass with the fix. |
| Repro minimization | verified | The one-test repro fails on 645d4fe14a20 with assertions on and off (two seeds) and passes with the fix; each assertion is tied to its own fix site. |
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 562 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,586 words
- Registry title
- On a zero-width geo3d path (GeoDegeneratePath), computePathCenterDistance with DistanceStyle.LINEAR_SQUARED or NORMAL_SQUARED returns the unsquared distance and computeOutsideDistance returns the fourth power, instead of the squared distance, because the 2022 path refactor converted the path components to aggregation form but not the two places that combine them (regression since 9.5.0)
- Area
- spatial3d / geom path distances (GeoDegeneratePath, the zero-width path; DistanceStyle aggregation forms)
- Priority
- Low, rank 883: The experimental squared distance styles return a wrong value only to direct callers, and it also needs a zero-width GeoDegeneratePath, so it is narrower.
- Impact class
- minor
- Impact note
- Only code that calls the experimental geom API GeoPath.computePathCenterDistance or computeOutsideDistance itself with DistanceStyle.LINEAR_SQUARED or NORMAL_SQUARED on a path whose width is below Vector.MINIMUM_ANGULAR_RESOLUTION (for example width 0, which GeoPathFactory.makeGeoPath turns into a GeoDegeneratePath) gets a wrong value, for every query point and at no special scale: d instead of d^2 from the path-centre distance, and d^4 from the outside distance for points within a chord of 1 planet radius (executed on SPHERE and WGS84); ARC, LINEAR and NORMAL are unaffected and no Lucene query, sort or comparator uses the squared styles, so nothing in-tree changes, but comparing a zero-width path with a path of width > 0 by path-centre distance, the comparison the GeoPath javadoc names, can rank them in the wrong order (executed).
- Summary
- You build a zero-width path with GeoPathFactory.makeGeoPath(planetModel, 0, points) and call computePathCenterDistance or computeOutsideDistance with DistanceStyle.LINEAR_SQUARED (or NORMAL_SQUARED), expecting the square of the LINEAR (or NORMAL) distance, which is what DistanceStyle's 'convert to aggregation form, aggregate, convert back' contract gives, what the same path with width 1e-6 returns and what Lucene 9.4.2 returned. Instead, for a point 0.01 rad beside the path, computePathCenterDistance returns about 0.01 (the unsquared distance) and computeOutsideDistance returns about 1e-8 (the fourth power) where about 1e-4 is expected. The cause is an incomplete edit in the 2022 path refactor: GeoDegeneratePath's components now return the square root (the aggregation form of the squared styles), but computePathCenterDistance never squares the minimum back, and PathSegment.outsideDistance still returns the plain square, which outsideDistance then squares a second time.
- Introduced
- 839dfb5a2dc46c4b2d16d9db5ea9f31ca1e8d907 ('More refactoring work, and fix a distance calculation.', GitHub kwrightapache (git author Karl David Wright), 2022-11-24T04:36:15Z; parent 5e4623af1fd), one commit of the #11965 spatial3d refactoring series, pushed directly to main with no pull request; the same patch went to branch_9x as cherry-pick f69fb6ed99b (identical git patch-id; parent cab1802308d). It wrapped SegmentEndpoint.pathCenterDistance, SegmentEndpoint.outsideDistance and PathSegment.pathCenterDistance in toAggregationForm and GeoDegeneratePath.outsideDistance's result in fromAggregationForm, but left computePathCenterDistance's return (unchanged since 8cb64ed926d, 2017) and PathSegment.outsideDistance unconverted. [exec: commit-level runs with the geom package of each commit shadowed over the 9.4.2 jars: the parents 5e4623af1fd and cab1802308d pass, 839dfb5a2dc and f69fb6ed99b fail 2 of 2 (JUnit) and 1 of 1 (main()); read: git blame in the history clone, GitHub commit metadata (read-only GET)] (2022-11-24 (839dfb5a2dc at 04:36:15Z, 2022-11-23 23:36 in the author's UTC-5 time zone; the branch_9x cherry-pick f69fb6ed99b committed at 04:46:56Z))
- First affected version
- 9.5.0 [exec] (tag 2023-01-30, through the branch_9x cherry-pick f69fb6ed99b; the candidate JUnit and a version-portable main() both fail); on the 10.x line 10.0.0 [exec]; 9.4.2 (tag 2022-11-21), the last release before the commit, passes [exec]
- Affected versions
- every release from 9.5.0 through 9.12.3 (14 releases) and from 10.0.0 through 10.5.2 (12 releases) and 10.6.0, and main through a9779ef3c193 (not fixed) [exec at 9.5.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0 (this doc's test, 2026-10-31) and main, -ea; main also -da; the other tags contain 839dfb5a2dc or f69fb6ed99b (git tag --contains) and carry the same half-converted GeoDegeneratePath.java (code-level scan of every 9.x and 10.x tag) [read]]; branch_10_6 (10.6.0, released 2026-10-29), the unreleased branch_10x and upstream main a95c754ee003 have the same code [read]. Not affected: 7.1.0 through 9.4.2, where the API exists and returns d^2 [exec at 7.1.0, 7.7.3, 8.11.4, 9.0.0 and 9.4.2; the 7.x and 8.x lines never received the refactor]; releases before 7.1.0 lack the API (zero-width paths, LUCENE-7955; computePathCenterDistance, LUCENE-7962; aggregation forms, LUCENE-7942; none exists in 7.0.1). All runs on JDK 25.0.3, aarch64 Linux; plain double arithmetic, x86 not checked.
- 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, code reading
- Upstream and prior reports
- none found (2026-10-27, read-only; the prior-report record, verdict new: 215 recorded queries, namely two scripted prior-report searches over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web, plus the finder's earlier run; manual GitHub searches on apache/lucene, Elasticsearch, OpenSearch, Solr and all of GitHub, and the full module:spatial3d label list (134 items); the changed files of all 410 open apache/lucene PRs; the upstream history of GeoDegeneratePath.java on five branches; direct searches over every lucene.apache.org and solr.apache.org list, with the 5 dev@ replies to the 2022 refactor commits read in full; LUCENE and SOLR JIRA queries; web queries; and a scan of all comments since 2022-11-20 on the 175 apache/lucene items found; 17 hits read, none the same bug). Related, not reports: apache/lucene#8991 (LUCENE-7942, 2017), which introduced the aggregation-form contract this bug breaks; #9004 (LUCENE-7955), which added GeoDegeneratePath; #9011 (LUCENE-7962), which added computePathCenterDistance; #11965, the tracking issue of the 2022 refactor that introduced the regression; #11975, a branch_9x compile fix in SegmentEndpoint.pathCenterDistance that keeps toAggregationForm; the dev@ replies to the introducing commit, about a precommit dead-code failure only. Not fixed upstream: GeoDegeneratePath.java on upstream main a95c754ee003 last changed in dfd536186fe (2026-04-01, mechanical), no commit after 2022-11-24 changes the two conversions on main, branch_10x, branch_10_6 or branch_9_12, and no open PR touches GeoDegeneratePath.java, GeoPath/GeoBasePath or the DistanceStyle classes. Gaps: GitHub search does not index review comments on PR diffs; closed unmerged PRs were covered by text search only; list search is keyword-based, so a report that uses none of the queried words could be missed.
- Lucene commit tested
- main
645d4fe14a20(commit of 2026-10-28): this doc's one-method test through gradle in a scratch worktree, with assertions on and off (seed DEADBEEF), and on the standalone runner; the same test on the 10.6.0, 10.5.2, 9.12.3 and 9.5.0 release jars (fails, assertions on) and 9.4.2 (passes). The hunt, verifiers, judge and follow-up ran on maina9779ef3c193(the cross-model reviews onea57360912ed); nothing inlucene/spatial3dchanged between those commits and645d4fe14a20. Fixes A and B were run through gradle on645d4fe14a20on 2026-10-31. Upstream main2f424fd428da(2026-10-30) still has both lines.
- Finding record
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegeneratePath.java:161-180 (at a9779ef3c193) (computePathCenterDistance: minimum of PathSegment and SegmentEndpoint pathCenterDistance, returned at :179 without fromAggregationForm), :271-287 (outsideDistance: minimum of SegmentEndpoint.outsideDistance and PathSegment.outsideDistance, then fromAggregationForm at :286), :522 and :536 (SegmentEndpoint.pathCenterDistance and outsideDistance return toAggregationForm(...)), :739 (PathSegment.pathCenterDistance returns toAggregationForm(...)), :713 (its pole branch returns a plain value), :866-874 (PathSegment.outsideDistance returns the plain Math.min(...) at :873), :33-35 (@lucene.internal, package-private class GeoDegeneratePath extends GeoBasePath)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/DistanceStyle.java:107-148 (at a9779ef3c193) (toAggregationForm, aggregateDistances and fromAggregationForm javadoc: 'Distances should be converted to aggregation form before aggregation is attempted, and they should be converted back from aggregation form to yield a final result'), :38 and :46 (public LINEAR_SQUARED and NORMAL_SQUARED), :22 (@lucene.experimental)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/LinearSquaredDistance.java:63-71 (at a9779ef3c193) and NormalSquaredDistance.java:63-71 (toAggregationForm = Math.sqrt, fromAggregationForm = square)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoStandardPath.java:278-285 (at a9779ef3c193) and :325-332 (the width > 0 sibling converts back from aggregation form in computePathCenterDistance and outsideDistance), :1593 (the same plain pole branch in PathSegment.pathCenterDistance)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPath.java:59-61 (at a9779ef3c193) and :72-74 (computePathCenterDistance: 'The distance is meant to allow comparisons between different paths to find the one that goes closest to a point'), :22 (@lucene.experimental)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPathFactory.java:38-44 (at a9779ef3c193) (makeGeoPath returns GeoDegeneratePath when maxCutoffAngle < Vector.MINIMUM_ANGULAR_RESOLUTION, else GeoStandardPath), :25 (@lucene.experimental)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBaseMembershipShape.java:46-53 (at a9779ef3c193) (computeOutsideDistance: 0.0 if isWithin, else outsideDistance) and GeoOutsideDistance.java:19-24 ('the distance from a point to the closest outside edge', @lucene.experimental)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPointDistanceComparator.java:85, (at a9779ef3c193) :131, :174 and Geo3DPointOutsideDistanceComparator.java:101, :144 (the in-tree sorts pass DistanceStyle.ARC); git grep finds LINEAR_SQUARED and NORMAL_SQUARED nowhere in lucene/ outside DistanceStyle.javalucene/CHANGES.txt:7842 (at a9779ef3c193) ('LUCENE-7955: Add support for zero-width paths.') and :7847 (LUCENE-7942: accurate aggregation of distances)
- The finding in 30 seconds
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPathFactory.java:25, (at 645d4fe14a20) :38-44lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegeneratePath.java:33-35, (at 645d4fe14a20) :160-180 (:179), :270-287 (:286), :522, :534-537, :712-714, :739, :866-874 (:873)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/LinearSquaredDistance.java:63-71; (at 645d4fe14a20) NormalSquaredDistance.java:63-71; DistanceStyle.java:36-38, :44-46, :107-148 (:116-118, :146-148)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoStandardPath.java:278-285, (at 645d4fe14a20) :326-333, :1592-1594; GeoBaseMembershipShape.java:46-53lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/Vector.java:26, (at 645d4fe14a20) :29 (MINIMUM_ANGULAR_RESOLUTION = PI * 1e-12)
- Who is affected
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPointDistanceComparator.java:85, (at 645d4fe14a20) :131, :174; Geo3DPointOutsideDistanceComparator.java:101, :144; Geo3DDocValuesField.java:365-374 (git grep LINEAR_SQUARED, NORMAL_SQUARED, computePathCenterDistance)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPath.java:22, (at 645d4fe14a20) :60-61, :73-74; GeoPathFactory.java:25; DistanceStyle.java:22; GeoOutsideDistance.java:23lucene/CHANGES.txt:7859 (at 645d4fe14a20) (LUCENE-7955 zero-width paths, under 7.1.0)
- Why it looks unintended
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/DistanceStyle.java:36-38, (at 645d4fe14a20) :44-46, :107-148; GeoStandardPath.java:278-285, :326-333; GeoOutsideDistance.java:19-24, :44-46; GeoPath.java:60-61, :73-74; LinearSquaredDistance.java:80, :85, :90lucene/CHANGES.txt:7864-7865 (at 645d4fe14a20) (LUCENE-7942, under 7.1.0)gh api repos/apache/lucene/issues/8991 (LUCENE-7942 description, read-only, 2026-10-31)lucene/spatial3d/src/test (at 645d4fe14a20) (git grep: no *_SQUARED, no computePathCenterDistance; computeOutsideDistance only in TestGeoBBox, TestGeoCircle, TestGeoConvexPolygon, TestRandomGeoPolygon)
- Suggested fix
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegeneratePath.java:179, (at 645d4fe14a20) :522, :536, :713, :739, :873, :286; GeoStandardPath.java:1593
- History
a Lucene clone releases/lucene/10.6.0 lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegeneratePath.java:179, :875 (same lines)gh api repos/apache/lucene/contents/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegeneratePath.java?ref=main and commits?path=... (read-only, 2026-10-31)
- Diagrams
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegeneratePath.java:179, (at 645d4fe14a20) :286, :873
- TestGeoDegeneratePathSquaredStyles.java (1 test):
repro/LFV-741/src/org/apache/lucene/spatial3d/geom/TestGeoDegeneratePathSquaredStyles.java(43 lines)
A picture of the minimal repro and the two missing conversions, made in parallel with this doc from the code at 645d4fe14a20; the PNG is the summary still.
assets/LFV-741/bug.png(562 KB)
