All findings · Lucene finding · LFV-674 · geo3ddistancecalculator-within-inverted

Geo3dDistanceCalculator.within returns the opposite answer: a point 1 degree away is not within 10 degrees

With the Geo3d spatial context, ctx.getDistCalc().within(from, x, y, limit) answers backwards: 1 degree away is "not within 10 degrees", 50 degrees away is "within". Lucene never calls within() itself; only application code that does (directly or through a hand-built CircleImpl) is hit, in every release since 7.1.0.

Summary

Priority
Low, rank 880Geo3dDistanceCalculator.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
minor
Since
7.1.0 (LUCENE-7951, 2017)introduced 2017-09-20 · about 9 years
Affected
7.1.0 to 10.6.0 and main
Status
documented
Found by
a randomized check of within() against the calculator's own distance()

AI-assisted page: this finding was first signalled by code reading or testing 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 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.

Background in 3 steps

  1. lucene/spatial-extras uses spatial4j's SpatialContext: a shape factory plus a DistanceCalculator. The usual context is SpatialContext.GEO; Geo3dSpatialContextFactory is an opt-in, experimental alternative backed by Lucene's spatial3d geometry.
  2. A DistanceCalculator has distance(from, toX, toY), in degrees, and within(from, toX, toY, distance), documented as "Returns true if the distance between from and to is <= distance".
  3. new Geo3dSpatialContextFactory().newSpatialContext() installs Geo3dDistanceCalculator as ctx.getDistCalc() unless you configure another calculator.
Diagram of the minimal repro with the Geo3d spatial context. From the origin (0, 0), the point (1, 0) is 1 degree away and the point (50, 0) about 50 degrees away; the limit is 10 degrees. The contract says within is true when the distance is at most the limit, so (1, 0) should be within and (50, 0) not. Geo3dDistanceCalculator.within returns limit < distance instead, so it answers false for (1, 0) and true for (50, 0).
The repro in one picture: 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.

The finding, step by step

  1. Create the Geo3d spatial context: new Geo3dSpatialContextFactory().newSpatialContext(). Its distance calculator, ctx.getDistCalc(), is a Geo3dDistanceCalculator, and distance() from (0, 0) to (1, 0) is 1.0 degree.
  2. Ask within(origin, 1, 0, 10): is (1, 0) within 10 degrees of (0, 0)? The spatial4j DistanceCalculator contract says within is true when the distance is <= the limit, so you expect true.
  3. 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.
  4. The method body is return (distance < distance(from, toX, toY));. The parameter distance is the limit, so it tests limit < actual distance, the exact opposite of actual distance <= limit, for every finite distance and non-NaN limit.
  5. 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 over PackedQuadPrefixTree those shapes over-match through a separate bug, LFV-673). Only code that calls within() itself, or builds spatial4j's CircleImpl by hand with this context, gets wrong answers.

Who is affected

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 mechanism (main 645d4fe14a20)

  1. 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 to PlanetModel.SPHERE (:33, :46-48) and, when no calculator is configured, sets distCalc = new Geo3dDistanceCalculator(planetModel) (:49-51). The configuration path (SpatialContextFactory.makeSpatialContext with spatialContextFactory set to this class) ends in the same method; initCalculator also installs it for distCalculator=geo3d (:78-88).
  2. 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.
  3. 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.

  1. Nothing in Lucene calls it. The in-tree main-source users of ctx.getDistCalc() call only distance() (SpatialStrategy.java:135-140, SpatialArgs.java:69, DistanceToShapeValueSource.java:76, ShapeFieldCacheDistanceValueSource.java:72-74, DistanceValueSource.java:75); the tests also call pointOnBearing, and nothing under lucene/, test or main, calls within on a calculator. In spatial4j 0.8 the only caller is CircleImpl.contains(double, double), used by CircleImpl.relate; the Geo3d shape factory's circle(...) returns a Geo3dCircleShape (Geo3dShapeFactory.java:176-196, :200-202), never a CircleImpl.

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.

The executed forms

call (sphere, from (0, 0))distance()expectedactual
within(origin, 1, 0, 10)1.0truefalse
within(origin, 50, 0, 10)49.99999999999999falsetrue
within(origin, 0, 0, 0)0.0truefalse
5,999 random checks within(from, to, lim) == (distance(from, to) <= lim), SPHERE, WGS84 and CLARKE_1866-all hold5,999 wrong
2,000 answers against spatial4j's geodesic calculator, limits 10% away from the distance-same answers2,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 is SpatialContext.GEO) and call ctx.getDistCalc().within(...). Solr exposes the Geo3d context through its spatialContextFactory="Geo3D" alias (SOLR-11382). A read-only GitHub code search found no call of getDistCalc().within other than spatial4j's own CircleImpl and 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 on within(), so nothing needs reindexing after the fix.
  • Not this bug: factory shapes on PackedQuad. In the same probe, factory circles on RPT over PackedQuadPrefixTree do over-match: circle(40, 40, 5) matched 419 documents where 7 are inside, and circle(-100, -30, 20) 428 where 49 are, with 0 within() calls. This fix leaves both numbers unchanged; only the fix of the sibling bug LFV-673 (Geo3dShape.relate with 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 inverted contains/relate answers. As an Intersects query on RPT over PackedQuadPrefixTree, CircleImpl(40, 40, 5) matched 419 documents where 7 are inside (7 with the fix), and CircleImpl(-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 a CircleImpl with a clean exception, and BBox with an UnsupportedOperationException, with or without the fix; PointVector filters by distance() and is exact.
  • Likelihood: rare. No report since 7.1.0 (2017).

Minimal repro

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.

TestGeo3dDistanceCalculatorWithin.java (1 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.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));
  }
}
Command
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).

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

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.

Environment

lucenemain 645d4fe14a20: affected; also the 10.6.0, 10.5.2 and 9.12.3 release jars with this test
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64

Observed output

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)

Why it looks unintended

  • spatial4j documents within as "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 &lt;= distance." (DistanceCalculator.java:29-30 in spatial4j 0.8). In 0.6 and 0.7, which 7.1.0 to 8.11.4 shipped with, AbstractDistanceCalculator.within has the same bytecode (checked in the binary jars; their javadoc text was not checked). AbstractDistanceCalculator.within is return 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 gives within no 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 within on this class. The introducing review (#9000 and lucene-solr#246) discussed other methods. The methods the class does not support throw UnsupportedOperationException (:104-117); within returns 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).

Suggested fix

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.

A: compare actual distance <= limit (validated by the checks) (recommended)

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:test with the fix and the repro (including TestGeo3d, 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, whose spatial-extras and spatial3d sources are identical to a9779ef3c193'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.

B: A, plus a clearer parameter name (not built)

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.

C: extend spatial4j's AbstractDistanceCalculator and drop the override (not built)

AbstractDistanceCalculator already implements within with <=.

Trade-offs

Changes the superclass of a public, if experimental, class to fix one line; A is smaller.

D: make within() throw, or deprecate it, instead of fixing it (not built)

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.

Downstream note

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.

How it was found

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 equal distance(from, to) <= lim, with lim just 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)) equals d, 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.

Prior reports, history and checks

Prior reports

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.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits2026-10-27the 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 historyonly the introducing issue #9000 and related Geo3d items
Open PRs2026-10-27the changed files of all 409 open apache/lucene PRsonly #12295 touches the file, and only distance()
Lists, JIRA, web2026-10-2756 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 queriesno report
Recheck (GitHub)2026-10-31Geo3dDistanceCalculator; Geo3dDistanceCalculator within; Geo3dSpatialContextFactory; DistanceCalculator within inverted; the upstream file historyonly #9000, #9134 and the unrelated 2012 proposal #5454; the file's last commit is still #15889 (distance() only)

History

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.

Versions and branches executed, with results
Version or commitVerdictResult
6.6.6, 7.0.0, 7.0.1 no such APIthe repro does not compile: Geo3dSpatialContextFactory and Geo3dDistanceCalculator do not exist yet
7.1.0 to 7.7.3 (12 releases) first: 7.1.0fail 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) affectedfail 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 affectedfail 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 affectedthis doc's test fails through gradle and the standalone runner, assertions on and off; passes with fix A

Independent checks

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.

Each independent check and its verdict
CheckVerdictWhat it found
Finder (a hunt unit) candidateRandomized 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 realOwn 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 verifiedThe one-test repro fails on 645d4fe14a20 with assertions on and off (seed DEADBEEF) and with seed 5EED1234 (assertions on), and passes with the fix.

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,201 words

Finding record

LFV-674, snapshot of 2026-11-06

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 main a9779ef3c193; nothing in lucene/spatial-extras or lucene/spatial3d changed 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 on 645d4fe14a20 on 2026-10-31. Upstream main 2f424fd428da (2026-10-30) still has the same line.

Lucene and upstream sources

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-77
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dSpatialContextFactory.java:28, (at 645d4fe14a20) :33, :39-53, :78-88
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShapeFactory.java:176-196, (at 645d4fe14a20) :200-202
  • lucene/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.java
  • lucene/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)

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

Repro files

About the diagrams

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.

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.