A spatial context bundles a shape factory and a distance calculator. The Geo3d context's calculator measures distances correctly, but its yes/no question "is this point within d of that one?" compares the two numbers the wrong way round.
lucene/spatial-extrasuses spatial4j'sSpatialContext: a shape factory plus aDistanceCalculator. The usual context isSpatialContext.GEO;Geo3dSpatialContextFactoryis an opt-in, experimental alternative backed by Lucene's spatial3d geometry.- A
DistanceCalculatorhasdistance(from, toX, toY), in degrees, andwithin(from, toX, toY, distance), documented as "Returns true if the distance between from and to is <= distance". new Geo3dSpatialContextFactory().newSpatialContext()installsGeo3dDistanceCalculatorasctx.getDistCalc()unless you configure another calculator.
distance() says (1, 0) is 1 degree from (0, 0), yet within(..., 10) answers no, because the code tests limit < distance instead of distance <= limit. Select the still to open it at full size. Generated with AI, checked against the code.- Create the Geo3d spatial context:
new Geo3dSpatialContextFactory().newSpatialContext(). Its distance calculator,ctx.getDistCalc(), is aGeo3dDistanceCalculator, anddistance()from (0, 0) to (1, 0) is 1.0 degree. - Ask
within(origin, 1, 0, 10): is (1, 0) within 10 degrees of (0, 0)? The spatial4jDistanceCalculatorcontract sayswithinis true when the distance is <= the limit, so you expect true. - You get false, with no error.
within(origin, 50, 0, 10)gives true although that point is 50 degrees away, and a point is not even within distance 0 of itself. - The method body is
return (distance < distance(from, toX, toY));. The parameterdistanceis the limit, so it tests limit < actual distance, the exact opposite of actual distance <= limit, for every finite distance and non-NaN limit. - Lucene's indexing, queries and distance sorts make 0
within()calls (measured over eight spatial strategies), so this bug does not change search results built from the context's own shapes (on RPT overPackedQuadPrefixTreethose shapes over-match through a separate bug, LFV-673). Only code that callswithin()itself, or builds spatial4j'sCircleImplby hand with this context, gets wrong answers.
Only applications that opt into the experimental Geo3d spatial context and call ctx.getDistCalc().within(...) themselves; for them every call with a non-NaN distance and limit is wrong, silently. Lucene's own indexing, queries and distance sorts never call it. The one search-result path found is a hand-built CircleImpl queried on RPT over the non-default PackedQuadPrefixTree: 419 hits where 7 are inside.
Mechanism, terms and numbers694 words
Geo3dSpatialContextFactory.newSpatialContext() installs a Geo3dDistanceCalculator as the context's distance calculator. Its within(from, toX, toY, distance) is return (distance < distance(from, toX, toY));: the parameter distance is the limit, so the line returns "limit < actual distance", the exact negation of spatial4j's documented "true if the distance between from and to is <= distance". Every answer with a finite distance and a non-NaN limit is inverted: from (0, 0), the point (1, 0) at 1.0 degree is "not within 10", the point (50, 0) at about 50 degrees is "within 10", and within(p, p, 0) is false.
The reach is narrow. No Lucene code calls DistanceCalculator.within: a counting calculator recorded 0 calls while indexing 2,000 points into eight strategies and running 63 queries and 7 distance sorts, so this bug does not change search results with shapes from the Geo3d shape factory, and no index data depends on it. (Those factory shapes do over-match on RPT over PackedQuadPrefixTree, 419 hits where 7 are inside, but through the separate Geo3dShape.relate bug LFV-673, which this fix does not change.) Applications that call ctx.getDistCalc().within(...) themselves get the opposite answer whenever neither the distance nor the limit is NaN. The one search-result path found needs application code that hand-builds spatial4j's CircleImpl with the Geo3d context: as a query on RPT over the non-default PackedQuadPrefixTree, such a circle matched 419 documents where 7 are inside.
The line has been there since the class was written (LUCENE-7951, 2017-09-20, first released in 7.1.0). Affected: 7.1.0 to 10.6.0 and main; not fixed upstream. Fix: return distance(from, toX, toY) <= distance; (one line).
- The Geo3d context installs this calculator by default. The factory constructor sets the Geo3d shape factory and binary codec (
Geo3dSpatialContextFactory.java:39-42).newSpatialContext()defaults the planet model toPlanetModel.SPHERE(:33,:46-48) and, when no calculator is configured, setsdistCalc = new Geo3dDistanceCalculator(planetModel)(:49-51). The configuration path (SpatialContextFactory.makeSpatialContextwithspatialContextFactoryset to this class) ends in the same method;initCalculatoralso installs it fordistCalculator=geo3d(:78-88). distance()is right.distance(Point from, double toX, double toY)returns the planet model's surface distance in degrees (Geo3dDistanceCalculator.java:54-72): 1.0 for (0, 0) to (1, 0) and 49.99999999999999 for (0, 0) to (50, 0). In an independent check its distances agreed with spatial4j's geodesic calculator to about 1e-14 degrees.within()compares the wrong way round (:74-77):
@Override
public boolean within(Point from, double toX, double toY, double distance) {
return (distance < distance(from, toX, toY));
}The parameter distance is the limit and distance(from, toX, toY) the method of step 2, so the line returns "limit < actual distance". When neither the distance nor the limit is NaN, that is the logical complement of "actual distance <= limit", the boundary actual == limit included. When either is NaN, both the stock line and the fix return false. The released bytecode (7.1.0 and main jars) is the same comparison; spatial4j's AbstractDistanceCalculator.within returns distance(from, toX, toY) <= distance.
- Nothing in Lucene calls it. The in-tree main-source users of
ctx.getDistCalc()call onlydistance()(SpatialStrategy.java:135-140,SpatialArgs.java:69,DistanceToShapeValueSource.java:76,ShapeFieldCacheDistanceValueSource.java:72-74,DistanceValueSource.java:75); the tests also callpointOnBearing, and nothing underlucene/, test or main, callswithinon a calculator. In spatial4j 0.8 the only caller isCircleImpl.contains(double, double), used byCircleImpl.relate; the Geo3d shape factory'scircle(...)returns aGeo3dCircleShape(Geo3dShapeFactory.java:176-196,:200-202), never aCircleImpl.
Changing only that line to return distance(from, toX, toY) <= distance; makes the repro pass on main, 10.5.2 and 7.1.0, so the comparison is the whole cause.
| call (sphere, from (0, 0)) | distance() | expected | actual |
|---|---|---|---|
within(origin, 1, 0, 10) | 1.0 | true | false |
within(origin, 50, 0, 10) | 49.99999999999999 | false | true |
within(origin, 0, 0, 0) | 0.0 | true | false |
5,999 random checks within(from, to, lim) == (distance(from, to) <= lim), SPHERE, WGS84 and CLARKE_1866 | - | all hold | 5,999 wrong |
| 2,000 answers against spatial4j's geodesic calculator, limits 10% away from the distance | - | same answers | 2,000 opposite |
With the fix the first three rows give true, false, true, and the random and reference checks all pass.
Exposure and likelihood334 words
- Silent, every call. For a direct caller every answer with a finite distance and a non-NaN limit is the opposite one, with no exception.
- Who is exposed. Applications that chose the opt-in Geo3d spatial context (
Geo3dSpatialContextFactory,@lucene.experimental; the usual spatial4j context isSpatialContext.GEO) and callctx.getDistCalc().within(...). Solr exposes the Geo3d context through itsspatialContextFactory="Geo3D"alias (SOLR-11382). A read-only GitHub code search found no call ofgetDistCalc().withinother than spatial4j's ownCircleImpland copies of it. - This bug does not change Lucene's own search results for shapes built by the Geo3d shape factory. A counting subclass installed as the context's calculator recorded 0
within()calls while indexing 2,000 points into eight strategies (RPT over Geohash, Quad, PackedQuad and S2, PointVector, BBox, SerializedDV, Composite), and across 63 queries and 7 distance sorts. No index data depends onwithin(), so nothing needs reindexing after the fix. - Not this bug: factory shapes on PackedQuad. In the same probe, factory circles on RPT over
PackedQuadPrefixTreedo over-match:circle(40, 40, 5)matched 419 documents where 7 are inside, andcircle(-100, -30, 20)428 where 49 are, with 0within()calls. This fix leaves both numbers unchanged; only the fix of the sibling bug LFV-673 (Geo3dShape.relatewith a plain rectangle) brings them to 7 and 51. The two bugs are separate. - The one search-result path found (uncommon). Application code that bypasses the shape factory with
new CircleImpl(center, radius, geo3dCtx)gets invertedcontains/relateanswers. As an Intersects query on RPT overPackedQuadPrefixTree,CircleImpl(40, 40, 5)matched 419 documents where 7 are inside (7 with the fix), andCircleImpl(-100, -30, 20)matched 428 where 49 are inside (52 with the fix, the normal grid approximation): every live document of one quarter-world cell. RPT over Geohash, Quad and S2, SerializedDV and Composite reject aCircleImplwith a clean exception, and BBox with anUnsupportedOperationException, with or without the fix; PointVector filters bydistance()and is exact. - Likelihood: rare. No report since 7.1.0 (2017).
One JUnit test in lucene/spatial-extras, public API only: build the Geo3d context the documented way and assert that (1, 0) is within 10 degrees of (0, 0). No index and no randomness; it fails with assertions on and off, and needs no build change.
/*
* 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.spatial.spatial4j;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.shape.Point;
public class TestGeo3dDistanceCalculatorWithin extends LuceneTestCase {
public void testWithin() {
SpatialContext ctx = new Geo3dSpatialContextFactory().newSpatialContext();
Point origin = ctx.getShapeFactory().pointXY(0, 0);
// spatial4j DistanceCalculator.within is true if the distance is <= the given limit
assertEquals(
"(1, 0) is 1 degree from (0, 0), so it is within 10 degrees",
true,
ctx.getDistCalc().within(origin, 1, 0, 10));
}
}
cd $LUCENE # an apache/lucene checkout at 645d4fe14a20 (or a later commit where Geo3dDistanceCalculator.java is unchanged)
cp $REPRO/TestGeo3dDistanceCalculatorWithin.java lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/
./gradlew :lucene:spatial-extras:test --tests org.apache.lucene.spatial.spatial4j.TestGeo3dDistanceCalculatorWithin -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false
- Expected
within(origin, 1, 0, 10)is true: the point is 1.0 degree away, which is <= 10.- Observed
(1, 0) is 1 degree from (0, 0), so it is within 10 degrees expected:<true> but was:<false>on main (assertions on and off) and on 10.6.0, 10.5.2 and 9.12.3 (assertions on).
More files, environment, full output143 words
The test sits in org.apache.lucene.spatial.spatial4j, next to the class, and uses only public methods. The result does not depend on the seed (it fails with DEADBEEF and 5EED1234). The modules it needs (spatial-extras, spatial3d, spatial4j, the test framework) are already on the module's test classpath.
The same file fails unchanged on the 10.6.0, 10.5.2 and 9.12.3 release jars (assertions on). For 9.0.0 and older the LuceneTestCase import is org.apache.lucene.util.LuceneTestCase; the history sweep ran that variant on every release from 7.1.0.
| lucene | main 645d4fe14a20: affected; also the 10.6.0, 10.5.2 and 9.12.3 release jars with this test |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
TestGeo3dDistanceCalculatorWithin > testWithin FAILED
java.lang.AssertionError: (1, 0) is 1 degree from (0, 0), so it is within 10 degrees expected:<true> but was:<false>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:73EFDB7B71364A45]: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:120)
at org.apache.lucene.spatial.spatial4j.TestGeo3dDistanceCalculatorWithin.testWithin(TestGeo3dDistanceCalculatorWithin.java:29)- spatial4j documents
withinas "true if the distance between from and to is <= distance", and its own base class does exactly that. - The Lucene class calls itself the "Geo3d implementation of DistanceCalculator", yet says a point is not within distance 0 of itself.
- Nothing documents or accepts the inversion; the methods the class does not support throw instead.
The full argument162 words
- The contract is explicit and has not changed. spatial4j
DistanceCalculator.within: "Returns true if the distance between from and to is <= distance." (DistanceCalculator.java:29-30in spatial4j 0.8). In 0.6 and 0.7, which 7.1.0 to 8.11.4 shipped with,AbstractDistanceCalculator.withinhas the same bytecode (checked in the binary jars; their javadoc text was not checked).AbstractDistanceCalculator.withinisreturn distance(from, toX, toY) <= distance;(:23-25). - The Lucene class claims that contract. Its javadoc is "Geo3d implementation of {@link DistanceCalculator}" (
Geo3dDistanceCalculator.java:30-34) and giveswithinno other meaning. The 7.1.0 CHANGES entry for LUCENE-7951 says spatial-extras now implements "Spatial4j abstractions: SpatialContextFactory, ShapeFactory, BinaryCodec, DistanceCalculator" (lucene/CHANGES.txt:7883-7885). - The boundary case is wrong too.
within(p, p, 0)is false at distance 0. - Nothing documents or accepts it. No test, CHANGES or MIGRATE entry mentions
withinon this class. The introducing review (#9000 and lucene-solr#246) discussed other methods. The methods the class does not support throwUnsupportedOperationException(:104-117);withinreturns a value.
The devil's-advocate check168 words
The strongest case for "intended" (the intended-behaviour check and the cross-model reviews): both classes are @lucene.experimental ("This API is experimental and might change in incompatible ways in the next release"); the package says it holds "Spatial4j stuff that ideally belongs in Spatial4j (isn't related to Lucene)"; the calculator is partly unimplemented; the contributor wrote on #9000 that "the DistanceCalculator needs to be reviewed"; nothing in Lucene calls within(); and nobody has complained in nine years.
Why it fails: experimental warns that the API may change, not that a method may return the opposite of its interface's documented result; the code ships in the regular spatial-extras module and CHANGES advertises it as a DistanceCalculator; "needs review" shows the calculator was unreviewed, not that the inversion was meant; no caller and no complaints limit the impact, which is why the class is minor. Verdict: not intended, a defect (the independent reproduction, the intended-behaviour check, the refute-lens and intended-lens cross-model reviews and the adjudication; the adjudication with high confidence).
Recommended A: compare actual distance <= limit
Compare the right way round: return distance(from, toX, toY) <= distance;, exactly what spatial4j's base class does (one line). The repro passes with assertions on and off, all 118 lucene/spatial-extras tests pass, and so do the module's lint checks. A NaN distance still gives false.
@Override
public boolean within(Point from, double toX, double toY, double distance) {
- return (distance < distance(from, toX, toY));
+ return distance(from, toX, toY) <= distance;
}All options and trade-offs4 options ยท 422 words
One option was built and validated through gradle on 645d4fe14a20; three alternatives are listed, not built.
1 line in Geo3dDistanceCalculator.java, the same body as spatial4j's AbstractDistanceCalculator.within.
@Override
public boolean within(Point from, double toX, double toY, double distance) {
- return (distance < distance(from, toX, toY));
+ return distance(from, toX, toY) <= distance;
}Trade-offs
None known. No Lucene code calls within(), so nothing in Lucene changes behaviour. An application that worked around the inversion by negating the result would have to drop its negation; none is known.
What the checks validated
- Gradle, scratch worktree of
645d4fe14a20, seed DEADBEEF: the repro fails on stock and passes with the fix, assertions on and off. - The whole
:lucene:spatial-extras:testwith the fix and the repro (includingTestGeo3d,TestGeo3dRpt,TestGeo3dShapeSphereModelRectRelation,TestGeo3dShapeWGS84ModelRectRelation): 27 suites, 118 tests, 5 skipped, 0 failures. :lucene:spatial-extras:check -x test(checkGoogleJavaFormat,ecjLintMain,ecjLintTest,forbiddenApisMain,forbiddenApisTest): passes.- Earlier, the same change made every verifier's test pass and fixed the probe's answers, and the finder's run of the full module with all six fixes from the same hunt (on main
ea57360912ed, whosespatial-extrasandspatial3dsources are identical toa9779ef3c193's) gave 117 tests, 0 failures, 5 skipped, as on the unpatched baseline. Shadowed into the 7.1.0 and 10.5.2 sources, it turns the repro from fail to pass.
Name the parameter maxDistance (return distance(from, toX, toY) <= maxDistance;). It removes the shadowing of the method name distance that makes the inverted line easy to misread; Java allows a different parameter name in an override.
Trade-offs
Cosmetic only.
AbstractDistanceCalculator already implements within with <=.
Trade-offs
Changes the superclass of a public, if experimental, class to fix one line; A is smaller.
For an experimental, partly unimplemented class a maintainer may prefer to make within throw UnsupportedOperationException, as area(...) and calcBoxByDistFromPt_yHorizAxisDEG already do (Geo3dDistanceCalculator.java:104-117), or to deprecate it.
Trade-offs
Throwing turns the silent wrong answer into a loud failure, but also makes a hand-built CircleImpl with this context throw from contains and relate (spatial4j's CircleImpl.contains calls within) and breaks any caller that uses within today; deprecating alone leaves the wrong answers in place. A is just as small and keeps the interface contract.
Lucene itself never calls within(); application code that calls it directly, for example to decide whether a point lies within some radius, gets every answer inverted, while other filter clauses and deletions in its queries are still honoured.
A randomized consistency check of the spatial4j bridge classes asked, for random point pairs on the sphere and the WGS84 ellipsoid, whether within(from, to, limit) agrees with distance(from, to) <= limit, with the limit just above or just below the distance. All 10,000 checks failed; with the one-line fix none did. No model checker or solver was involved.
The method in full121 words
Method: non-formal (a hunt unit, a differential check of the spatial4j-to-spatial3d bridge; the finder tagged it semi-formal, the registry records it as non-formal).
- The harness checked self-consistency properties of the Geo3d bridge. Family G4a:
within(from, to, lim)must equaldistance(from, to) <= lim, withlimjust above or just below the measured distance, on SPHERE and WGS84 (seed 5EED220): 5,000 of 5,000 failures per model on stock main, 0 with the fix. The sibling family G4b,distance(from, pointOnBearing(from, d, b))equalsd, was clean. - 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: 96 recorded searches over GitHub, the mailing lists, JIRA, downstream repositories and the web, plus the files of all 409 open PRs (2026-10-27), rechecked on 2026-10-31. Closest: LUCENE-7951 (#9000), which added the line; its review never discussed within.
Searches4 searches
Verdict new: nothing reports or discusses Geo3dDistanceCalculator.within returning the opposite answer, and no fix exists. Related, not reports of this bug: #9000 / LUCENE-7951 and lucene-solr#246 (introduced the line; the contributor asked for the calculator to be reviewed, the review covered other methods), #9134 / LUCENE-8086 and lucene-solr#288 (reworked pointOnBearing; within only as a diff-hunk header), #12295 (open; changes only distance()), SOLR-11382 (Solr's Geo3D alias), spatial4j#134 and #82 (feature requests). Gaps: GitHub search matches tokens, not substrings; the web engine ignored exact-term quotes; private downstream trackers cannot be searched.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits | 2026-10-27 | the scripted prior-report search in two runs (45 and 51 queries, 217 distinct hits in the second); manual searches of all of GitHub for Geo3dDistanceCalculator, Geo3dSpatialContextFactory and Geo3dPointShape; the apache/lucene, apache/solr and locationtech/spatial4j trackers; the file's commit history | only the introducing issue #9000 and related Geo3d items |
| Open PRs | 2026-10-27 | the changed files of all 409 open apache/lucene PRs | only #12295 touches the file, and only distance() |
| Lists, JIRA, web | 2026-10-27 | 56 queries over the Lucene and Solr dev and user lists; LUCENE and SOLR JIRA, with the description and all comments of nine later Geo3D issues (filed 2017-08 to 2020) searched for the calculator; 6 web queries | no report |
| Recheck (GitHub) | 2026-10-31 | Geo3dDistanceCalculator; Geo3dDistanceCalculator within; Geo3dSpatialContextFactory; DistanceCalculator within inverted; the upstream file history | only #9000, #9134 and the unrelated 2012 proposal #5454; the file's last commit is still #15889 (distance() only) |
Introduced with the class by LUCENE-7951 (2017-09-20, first released in 7.1.0); the line was never edited. Executed: all 72 releases from 7.1.0 to 10.5.2 fail, and so do 10.6.0 and main; 7.0.1 and older have no such class. Not fixed upstream.
Versions tested5 rows
Introduced by LUCENE-7951 "New wrapper classes for Geo3d" (#9000): main 035523ba7a86 and the branch_7x cherry-pick 85491401d54c, both 2017-09-20, "LUCENE-7951: Spatial4j implementations using Geo3d". The code came from lucene-solr#246 by iverase, committed as a patch by dsmiley. The inverted line is in the first version of the file; git log -L on the method and git blame -w give only this commit. Later commits (LUCENE-8086 pointOnBearing, the LUCENE-9570 reformat, #15889 instanceof patterns in distance()) never touch within(). Not a regression: no release ever had a correct within() in this class. Upstream main 2f424fd428da (2026-10-30) still has the line at 76.
| Version or commit | Verdict | Result |
|---|---|---|
| 6.6.6, 7.0.0, 7.0.1 | no such API | the repro does not compile: Geo3dSpatialContextFactory and Geo3dDistanceCalculator do not exist yet |
| 7.1.0 to 7.7.3 (12 releases) | first: 7.1.0 | fail 1 of 1 with the same message; 7.1.0 passes with only the one-line fix shadowed in |
| 8.0.0 to 8.11.4 (27), 9.0.0 to 9.12.3 (21) | affected | fail 1 of 1 with the same message; this doc's test also fails on 9.12.3 |
| 10.0.0 to 10.5.2 (12), 10.6.0 | affected | fail 1 of 1; 10.5.2 passes with only the one-line fix shadowed in; this doc's test fails on 10.6.0 and 10.5.2 |
| main 645d4fe14a20 | affected | this doc's test fails through gradle and the standalone runner, assertions on and off; passes with fix A |
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 verdicts294 words
Before the finding was recorded, an executed refute review by OpenAI GPT-5.6 reproduced it with its own test (limits above, below and equal to the distance) and rated the intended-behaviour argument weak.
| Check | Verdict | What it found |
|---|---|---|
| Finder (a hunt unit) | candidate | Randomized consistency check: 10,000 of 10,000 wrong on main, 0 with the fix; own JUnit fails 1 of 1 on main (assertions on and off) and on 10.5.2, 9.12.3, 8.11.4, 7.7.3 and 7.1.0 (assertions on), and passes with the fix; the follow-up later ran it with assertions off on 7.1.0, 9.12.3 and 10.5.2 (fails). |
| Independent reproducer | real | Own JUnit outside Lucene's packages fails 4 of 5 on main (assertions on and off), 10.5.2 and 9.12.3 (the pass is a sanity check): within(p, p, 0) false, 5,999 of 5,999 random checks wrong, 2,000 of 2,000 opposite to spatial4j's calculator; passes 5 of 5 with its own one-line fix. |
| Intended lens (OpenAI GPT-5.6, executed) | real (high) | Own test shows the exact negation below, at and above the distance; fails with assertions on and off and passes with the fix, re-run on a9779ef3c193. |
| Devil's advocate | real (high) | Built the strongest intended case and rejected it; own JUnit, with the context built from configuration, fails 2 of 3 on main, 10.5.2 and 9.12.3 (spatial4j control passes); 3 of 3 pass with the fix. |
| Adjudication | confirmed (high) | Re-ran the independent reproduction's JUnit (4 of 5 fail, assertions on and off), the finder's (1 of 1 fail) and the independent reproduction's with the fix (5 of 5 pass). |
| Repro minimization | verified | The one-test repro fails on 645d4fe14a20 with assertions on and off (seed DEADBEEF) and with seed 5EED1234 (assertions on), and passes with the fix. |
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 479 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,201 words
- Registry title
- Geo3dDistanceCalculator.within returns the opposite of 'distance <= limit' for every finite distance, so the default distance calculator of the Geo3d spatial context answers every within() call backwards
- Area
- spatial-extras / spatial4j bridge to spatial3d (Geo3dDistanceCalculator.within, the default DistanceCalculator that Geo3dSpatialContextFactory installs)
- Priority
- Low, rank 880: Geo3dDistanceCalculator.within answers every call backwards, but Lucene's own queries and sorts never call it, so it matches the narrow direct-caller profile of LFV-271.
- Impact class
- minor
- Impact note
- Only applications that use the opt-in, @lucene.experimental Geo3d spatial context (Geo3dSpatialContextFactory) and call ctx.getDistCalc().within(...) themselves get the opposite answer, on every call with a finite distance and at ordinary scale; Lucene's own indexing, queries and distance sorts never call within() (0 calls in an executed probe over eight spatial strategies), so this bug does not change search results with shapes from the Geo3d shape factory (their over-match on RPT over PackedQuadPrefixTree is the separate LFV-673), and the one search-result path found needs application code that builds spatial4j's CircleImpl by hand with the Geo3d context and queries RecursivePrefixTreeStrategy over the non-default PackedQuadPrefixTree (419 hits where the exact answer is 7); every release from 7.1.0 to 10.6.0.
- Summary
- You create a spatial context with Geo3dSpatialContextFactory and ask its distance calculator whether the point (1, 0) is within 10 degrees of (0, 0), with ctx.getDistCalc().within(from, 1, 0, 10), and expect true: distance() returns 1.0 degree, and the spatial4j DistanceCalculator contract says within() is true when the distance is <= the limit. Instead it returns false, and within(from, 50, 0, 10) returns true although the distance is 50 degrees. The method body is 'return (distance < distance(from, toX, toY));', the exact negation of the contract, so every answer with a finite distance is inverted, including within(p, p, 0).
- Introduced
- 035523ba7a86773500c053129f65c9a6dc4fd384 (main) and its branch_7x cherry-pick 85491401d54c59e1593a5c9f13dca7bfcdda31a2 (the commit that shipped in 7.1.0): LUCENE-7951 'Spatial4j implementations using Geo3d' (issue apache/lucene#9000), code from apache/lucene-solr#246 by GitHub iverase (closed unmerged 2017-09-29, committed as a patch by GitHub dsmiley) [read: git log -L on the method and git blame -w of lines 74-77 in the full-history clone give only this commit, and the inverted line is in the first version of the file; exec: shadowing only the one-line fix into the 7.1.0 source flips the repro from fail to pass; no commit-level bisection needed]. Not a regression: no release ever had a correct within() in this class. Later commits to the file (d66d9549d7c LUCENE-8086, 2cbf261032d LUCENE-9570, dfd536186fe #15889) do not touch within(). (2017-09-20 (2017-09-21T03:09:46Z; main and branch_7x the same day); first release 7.1.0)
- First affected version
- 7.1.0 [exec: the finder's JUnit fails 1 of 1 with -ea and -da, and passes with only the one-line fix shadowed into the 7.1.0 source]; 6.6.6, 7.0.0 and 7.0.1 have no such API [exec: Geo3dSpatialContextFactory and Geo3dDistanceCalculator are absent, so the repro does not compile]
- Affected versions
- Every release from 7.1.0 to 10.6.0 and main a9779ef3c193; not fixed upstream. [exec, JDK 25, SHA-1-checked release jars, seed DEADBEEF] the one-test repro fails on the 10.6.0 jars (2026-10-31); the finder's JUnit fails 1 of 1 on all 72 releases from 7.1.0 to 10.5.2 (12 in 7.x, 27 in 8.x, 21 in 9.x, 12 in 10.x) and on main with -ea and -da; the independent reproduction's and the intended-behaviour check's own tests also fail on 10.5.2 and 9.12.3. [read] the line is unchanged on upstream main a95c754ee003 (2026-10-26), branch_10x, branch_10_6 and branch_10_5; the only open PR that touches the file (#12295) changes only the two distance() methods.
- 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
- non-formal
- Upstream and prior reports
- none found (searched 2026-10-27, read-only; the prior-report record, verdict new): two scripted prior-report searches (45 queries with the web part pending, then 51 queries over GitHub issues and PRs on apache/lucene, commits, downstream repositories, the dev, java-user, issues and commits lists, JIRA and the web: 217 distinct hits, the introducing issue #9000 the only high hit), manual GitHub searches (all of GitHub for Geo3dDistanceCalculator, Geo3dSpatialContextFactory and Geo3dPointShape; phrasings on apache/lucene, apache/solr and locationtech/spatial4j), the changed files of all 409 open apache/lucene PRs (only #12295 touches Geo3dDistanceCalculator.java, and it changes only distance()), manual JIRA queries (LUCENE and SOLR, plus the full text of nine post-2017 Geo3D issues), 56 list queries over the Lucene and Solr user and dev lists, and 6 web queries. Nothing reports or discusses Geo3dDistanceCalculator.within returning the opposite of 'distance <= limit', and no fix exists: upstream main a95c754ee003 (2026-10-26), branch_10x and branch_10_5 still have the line. Closest: the introducing issue LUCENE-7951 / #9000 and its PR lucene-solr#246 (the contributor wrote that 'the DistanceCalculator needs to be reviewed'; the review discussed other methods), LUCENE-8086 / #9134 with lucene-solr#288 (reworked pointOnBearing; within appears only as a diff-hunk header), SOLR-11382 (Solr Geo3D alias, no mention of the calculator), and spatial4j#134 and #82 (feature requests). Gaps: GitHub search matches tokens, not substrings; the web engine ignored exact-term quotes, so web coverage is weak; private downstream trackers cannot be searched.
- 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 and 9.12.3 release jars with assertions on (fails). The hunt, verifiers, judge and follow-up ran on maina9779ef3c193; nothing inlucene/spatial-extrasorlucene/spatial3dchanged between the two commits. The follow-up's sweep executed the finder's test on all 72 releases from 7.1.0 to 10.5.2 (all fail). The one-line fix was run through gradle on645d4fe14a20on 2026-10-31. Upstream main2f424fd428da(2026-10-30) still has the same line.
- Finding record
lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dDistanceCalculator.java:30-35 (at a9779ef3c193) (class javadoc 'Geo3d implementation of DistanceCalculator', @lucene.experimental), :74-77 (within: 'return (distance < distance(from, toX, toY));')lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dSpatialContextFactory.java:28 (at a9779ef3c193) (@lucene.experimental), :45-53 (newSpatialContext installs new Geo3dDistanceCalculator(planetModel) as distCalc when none is configured)SECURITY.md (in-scope and out-of-scope sections, as cited in the security record)
- The finding in 30 seconds
lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dDistanceCalculator.java:30-35, (at 645d4fe14a20) :54-72, :74-77lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dSpatialContextFactory.java:28, (at 645d4fe14a20) :33, :39-53, :78-88lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShapeFactory.java:176-196, (at 645d4fe14a20) :200-202lucene/spatial-extras/src/java/org/apache/lucene/spatial/SpatialStrategy.java:135-140; (at 645d4fe14a20) query/SpatialArgs.java:69; util/DistanceToShapeValueSource.java:76; util/ShapeFieldCacheDistanceValueSource.java:72-74; vector/DistanceValueSource.java:75 (git grep getDistCalc, .within)
- Who is affected
lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dSpatialContextFactory.java:28; (at 645d4fe14a20) lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/tree/PackedQuadPrefixTree.java:400 (cells are plain RectangleImpl)
- Why it looks unintended
lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dDistanceCalculator.java:30-34, (at 645d4fe14a20) :104-117; lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/package-info.javalucene/CHANGES.txt:7883-7885 (at 645d4fe14a20) (LUCENE-7951 under Lucene 7.1.0)Lucene main 645d4fe14a20 build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/RenderJavadocPlugin.java:502 (lucene.experimental text)
- Suggested fix
spatial4j 0.8 AbstractDistanceCalculator.java:23-25
- Prior reports
gh api search/issues (repo:apache/lucene, four queries) and repos/apache/lucene/commits?path=lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dDistanceCalculator.java (read-only, 2026-10-31)
- History
a Lucene clone releases/lucene/10.6.0 lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dDistanceCalculator.java:76 (same line)gh api repos/apache/lucene/contents/lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dDistanceCalculator.java?ref=main and commits?path=... (read-only, 2026-10-31)
- Diagrams
lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dDistanceCalculator.java:74-77 (at 645d4fe14a20)
- TestGeo3dDistanceCalculatorWithin.java (1 test):
repro/LFV-674/src/org/apache/lucene/spatial/spatial4j/TestGeo3dDistanceCalculatorWithin.java(34 lines)
A picture of the minimal repro and the inverted comparison, made in parallel with this doc from the code at 645d4fe14a20; the PNG is the summary still.
assets/LFV-674/bug.png(479 KB)
