All findings · Lucene finding · LFV-301 · pointvector-dateline-opposite-sign-edge

PointVectorStrategy misses a point at longitude ±180° when a rectangle's dateline edge is written with the other sign

Longitude -180 and +180 are the same meridian, but PointVectorStrategy filters a box that only touches the dateline with a plain number range. So the western hemisphere rect(-180, 0, -90, 90) finds 0 hits for a point at (180, 10) instead of 1, on all 14 releases tested from 4.0.0 to 10.5.2 and on main.

Summary

Priority
Low, rank 423The experimental PointVectorStrategy silently misses points that lie exactly on the antimeridian when a box edge has the opposite sign, a boundary-value geo miss that is rarer than LFV-316.
Impact
silent wrong results
Since
4.0.0 (2012, the spatial4j 0.3 upgrade)introduced 2012-09-05 · over 14 years
Affected
4.0.0 to 10.6.0 (10.6.0 by reading) and main
Status
documented
Found by
bounded-exhaustive comparison of the real jar with spatial4j relate

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

The finding in 30 seconds

PointVectorStrategy stores a point's longitude (x) and latitude (y) as two plain numbers and answers a rectangle query with number ranges. A box with minX > maxX crosses the dateline and gets two x ranges; every other box gets one range, x in [minX, maxX], so +180 and -180 are different values to it.

Summary card. Panel 1: a longitude line from -180 to 180 with the western-hemisphere box rect(-180, 0, -90, 90) and the point (180, 10) on the dateline; expected 1 hit, actual 0 hits, BBoxStrategy 1 hit. Panel 2, inside Lucene: x is stored as the plain number 180.0, the box does not cross the dateline, so the query is one range pv__x:[-180.0 TO 0.0] and 180.0 is outside it, while spatial4j relate says WITHIN; also missed: the eastern box with (-180, 10) and the caller's rect(180, -170, ...); the candidate fix finds all three. Panel 3: when it happens, and what is not affected.
The western-hemisphere box from -180 to 0 and a point stored at longitude 180 on the same meridian: the query becomes one range, x in [-180, 0], so the point is missed (0 hits; expected and BBoxStrategy: 1). Select the still to open it at full size. Generated with AI, checked against the code.
  1. Index one point at longitude 180, latitude 10 with PointVectorStrategy in the standard geo context SpatialContext.GEO (default field type, codec and IndexWriterConfig).
  2. Search the western hemisphere: Intersects (or IsWithin) rect(-180, 0, -90, 90). You expect 1 hit: -180 and +180 are one meridian, SpatialOperation.evaluate says the point is inside, and BBoxStrategy returns 1.
  3. You get 0 hits, with no error. The box does not cross the dateline, so the query is one range, x in [-180, 0], and the stored 180.0 is outside it.
  4. Writing +180 everywhere does not help: spatial4j stores the caller's rect(180, -170, -10, 10) as minX = -180, and it misses (180, 0) too. The mirror case fails the same way: the eastern hemisphere rect(0, 180, -90, 90) misses (-180, 10).
  5. Only missed matches: across 570 boxes x 35 grid points the strategy misses 420 point-box pairs per operation, all at ±180 with the other sign, and returns 0 extra hits.

Who is affected

Applications that index points with the experimental PointVectorStrategy in a geo context and run rectangle Intersects or IsWithin queries, with points exactly at ±180. Deterministic with default settings, assertions on or off; the miss follows from the query, not the index size. Only missed matches, never extra ones. Reach is low: Solr removed its wrapper field type in 9.0.

Mechanism, terms and numbers680 words

In a geo SpatialContext, longitude -180 and +180 are the same meridian, and spatial-extras defines Intersects and IsWithin through spatial4j's relate, which treats them as one. PointVectorStrategy stores a point's x as a plain DoublePoint and, for a rectangle that does not cross the dateline, filters x with one closed range [minX, maxX]. To that range +180 and -180 are different numbers: a point stored at +180 is not found by a box whose west edge is -180, and a point at -180 is not found by a box whose east edge is +180. The western hemisphere rect(-180, 0, -90, 90) returns 0 hits for (180, 10), the eastern hemisphere rect(0, 180, -90, 90) 0 hits for (-180, 10), and rect(180, -170, -10, 10), written with +180 by the caller, 0 hits for (180, 0), because spatial4j itself turns that box's west edge into -180. The oracle and BBoxStrategy return 1 in every case. Both operations fail, with assertions on and off; the miss follows from the query itself, not from the index size (executed with one document and with a 609-document, 7-segment index). Only missed matches, never extra ones. It reproduces on all 14 releases tested from 4.0.0 (2012, when the module moved to spatial4j 0.3) to 10.5.2 and on main. A 6-line fix in makeWithin that also matches the other-sign meridian passes the repro and all spatial-extras tests.

Terms

  • Geo context: spatial4j's SpatialContext.GEO, the standard context for longitude/latitude in degrees. Longitude runs from -180 to +180, and both values name the same meridian, the antimeridian or dateline.
  • PointVectorStrategy (spatial-extras, @lucene.experimental): indexes one point per document as two DoublePoint fields (x and y) plus doc values, and supports Intersects and IsWithin queries by a rectangle or a circle.
  • Crossing box: a spatial4j rectangle with minX > maxX, for example rect(170, -170), which covers 170 to 180 and -180 to -170. A box with an edge on ±180 that does not wrap, such as rect(-180, 0), is not crossing.
  • The oracle: SpatialOperation.evaluate(indexed, query), which is spatial4j's relate. It defines the predicates for every strategy in the module, and in a geo context it treats x = +180 and x = -180 as one meridian.

What happens, step by step (main a9779ef3c193, PointVectorStrategy.java)

  1. createIndexableFields(Point) (:163-180) stores x as given: new DoublePoint(fieldNameX, point.getX()) (:175). spatial4j keeps 180 as 180.0.
  2. makeQuery (:187-206) sends a rectangle to makeWithin (:209-222). Only a crossing box gets two SHOULD ranges, [-inf, maxX] or [minX, +inf] (:212-216); any other box gets one MUST range [minX, maxX] on x (:217-219), closed at both ends (rangeQuery, :228-242).
  3. For the western hemisphere the query is +pv__x:[-180.0 TO 0.0] +pv__y:[-90.0 TO 90.0]. The stored x is 180.0, outside [-180, 0], so the document is missed, while relate says WITHIN.
  4. A non-crossing box lies inside [-180, 180], so a point at ±180 can only sit on one of its edges, and only the opposite sign is wrong. A crossing box already accepts both signs.
  5. spatial4j writes edges with either sign itself: rect(180, -170) becomes minX = -180, rect(170, -180) becomes maxX = 180, both non-crossing. So no single storage convention avoids the miss.

Examples (executed on main, -ea and -da)

pointquery box (as stored)oracleBBoxStrategyPointVectorStrategy
(180, 10)western hemisphere rect(-180, 0, -90, 90)inside10
(-180, 10)eastern hemisphere rect(0, 180, -90, 90)inside10
(180, 0)caller's rect(180, -170, -10, 10), stored as minX = -180inside10
(-180, 10)rect(-180, 0, -90, 90) (same sign)inside11
(180, 0) and (-180, 0)rect(170, -170, -10, 10) (crosses)inside11
(179.9, 0)rect(-180, -170, -10, 10)outside00

IsWithin gives the same answers as Intersects on all of these. Circle queries share the bounding-box filter, but a circle's box meets ±180 only at its tangent point, where spatial4j's own answer is rounding-sensitive; no circle miss was found, so circles are not claimed.

Exposure and likelihood339 words
  • Who is exposed. Users of PointVectorStrategy (@lucene.experimental) in a geo context, typically SpatialContext.GEO, with rectangle Intersects or IsWithin queries. newInstance(ctx, prefix), the default field type, codec and IndexWriterConfig are enough (an independent probe with a plain ByteBuffersDirectory and new IndexWriterConfig() misses the same inputs). Only field types with point values (the default) are affected; without them every rectangle query throws "An index is required for this operation." (PointVectorStrategy.java:241).
  • What it takes. A point stored at exactly longitude +180 or -180, and a box that does not cross the dateline with its edge on that meridian written, or normalized by spatial4j, with the other sign. Box edges on ±180 are ordinary (hemisphere boxes, tiles ending at the dateline); points exactly on the antimeridian are uncommon but real (snapped or rounded coordinates, grid data).
  • What goes wrong. 0 hits where the oracle says 1. A probe of 9,000,000 checks (4.5 million random point-box pairs, each for both operations) found 0 extra hits against an exact reference; its 96 disagreements with spatial4j relate are rounding in spatial4j on crossing boxes, where the strategy gives the exact answer. No error, and nothing stored is wrong, so a fix needs no reindexing.
  • Not affected. Every other strategy class in the module that indexes geo points returns the hit on these inputs: BBoxStrategy, SerializedDVStrategy, CompositeSpatialStrategy, and the prefix-tree strategies on geohash, quad and packed-quad grids (as configured in the probes; these trees are approximate grids, and one quad configuration also matches the outside control at 179.9). Also not affected: boxes that cross the dateline, same-sign edges, the world box. Core LatLonPoint is a different API with its own convention and is not claimed.
  • Reach. Less-used strategy: Solr deprecated its PointVectorStrategy field type in 6.6 and removed it in 9.0 (SOLR-10729, SOLR-13138); a code search of the default branches of Elasticsearch, OpenSearch and Solr found no use (re-run 2026-10-28 with a positive control per repository). No report of this bug was found in the public sources searched.

Minimal repro

The test shows that PointVectorStrategy (geo context) misses a point at longitude +180 for an Intersects box whose west edge is -180, the same meridian.

TestPointVectorDatelineEdge.java (JUnit test)57 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.vector;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.shape.Shape;

public class TestPointVectorDatelineEdge extends LuceneTestCase {

  public void testPointAtPlus180InBoxFromMinus180() throws Exception {
    SpatialContext ctx = SpatialContext.GEO;
    PointVectorStrategy strategy = PointVectorStrategy.newInstance(ctx, "pv");
    Shape box = ctx.getShapeFactory().rect(-180, -170, -10, 10);
    try (Directory dir = new ByteBuffersDirectory();
        IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
      Document doc = new Document();
      for (Field f : strategy.createIndexableFields(ctx.getShapeFactory().pointXY(180, 0))) {
        doc.add(f);
      }
      w.addDocument(doc);
      try (DirectoryReader reader = DirectoryReader.open(w)) {
        IndexSearcher searcher = new IndexSearcher(reader);
        // geo: x=+180 is the box's west edge x=-180 (same meridian): expected 1 hit, actual 0
        assertEquals(
            "Intersects rect(-180,-170,-10,10) with Pt(180,0) indexed",
            1,
            searcher.count(
                strategy.makeQuery(new SpatialArgs(SpatialOperation.Intersects, box))));
      }
    }
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-301/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble   (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/spatial-extras/build/libs/lucene-spatial-extras-$V.jar
CP=$CP:$LUCENE/lucene/spatial3d/build/libs/lucene-spatial3d-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar   # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar   # JUnit Platform console launcher (Maven Central)

javac -d classes -cp "$CP" $(find src -name '*.java')

# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
     -cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
     execute --select-class org.apache.lucene.spatial.vector.TestPointVectorDatelineEdge
# the module(s) spatial-extras also need their third-party runtime jars
Expected vs actual
Expected 1 hit for rect(-180,-170,-10,10) with Pt(180,0) indexed; actual 0 (expected:<1> but was:<0>), with -ea and -da.

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

More files, environment, full output38 words

Environment

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

Observed output

# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
    => java.lang.AssertionError: Intersects rect(-180,-170,-10,10) with Pt(180,0) indexed expected:<1> but was:<0>
       org.junit.Assert.fail(Assert.java:89)
       org.junit.Assert.failNotEquals(Assert.java:835)
       org.junit.Assert.assertEquals(Assert.java:647)
       org.apache.lucene.spatial.vector.TestPointVectorDatelineEdge.testPointAtPlus180InBoxFromMinus180(TestPointVectorDatelineEdge.java:49)

Why it looks unintended

  • spatial-extras defines Intersects and IsWithin through spatial4j relate, which treats -180 and +180 as one meridian; BBoxStrategy handles this case ("-180 == 180").
  • TestBBoxStrategy has dateline-edge tests (LUCENE-5714): a line on +180, rect(180,180), must be within rect(-180,-100).
  • Storing points with one sign does not avoid it: spatial4j itself stores the caller's rect(180, -170) with minX = -180.
The full argument265 words
  • The contract. The class javadoc: "Can query by a rectangle or circle" and "SpatialOperation#Intersects and SpatialOperation#IsWithin is supported" (PointVectorStrategy.java:62-64). SpatialStrategy.makeQuery: "Make a Query based principally on SpatialOperation and Shape" (SpatialStrategy.java:116-117). SpatialOperation: "For more explanation of each predicate, consider looking at the source implementation of evaluate" and "Lucene-spatial makes no distinction of shape boundaries" (SpatialOperation.java:30-34), so a point on an edge counts.
  • The oracle. evaluate is spatial4j relate, which in a geo context says (180, 0) is within rect(-180, -170, -10, 10), for points already from spatial4j 0.3 on (the 4.0.0 run prints WITHIN). In locationtech/spatial4j#85, found through Lucene's TestBBoxStrategy, spatial4j extended the convention to zero-width boxes on the dateline, choosing to "deem longitude +/- 180 zero width as equal longitudinally".
  • The module states the convention elsewhere. BBoxStrategy.java:387,395: "touches dateline; -180 == 180". TestBBoxStrategy.java:108-175: testIntersectsBugDatelineEdge, testWithinBugDatelineEdge, testContainsBugDatelineEdge, testAlongDatelineOppositeSign, added with LUCENE-5714 (#6776), whose randomized tests "uncovered dateline edge cases (e.g. touches but doesn't cross the dateline) and world-wrap (-180 to 180) bugs", fixed then in BBoxStrategy only. TestRandomSpatialOpFuzzyPrefixTree.java:518-519: "in geo, -180 & +180 are the same place".
  • Every other strategy class in the module that indexes geo points returns the hit on these inputs.
  • History. LUCENE-4389 (#5455, 2012) added the crossing branch so that "at least it won't be buggy if you stay with Rectangle query shapes"; it handled only boxes that cross. No javadoc, CHANGES entry, MIGRATE note or test asserts the current behaviour, and the 22 upstream tests that use PointVectorStrategy pass with the fix.
The devil's-advocate check233 words

The strongest case for "intended" (an intended-behaviour checker and a cross-model reviewer): the class is @lucene.experimental and says "This is a simple Strategy. Search works with a pair of range queries on two DoublePoints" (PointVectorStrategy.java:72-73), so read literally x is a plain number and callers should canonicalize. Core LatLonPoint also treats 180 and -180 as different, and in #9767 (core LatLonShape) a committer called the same kind of opposite-sign behaviour "more like a feature". In #10728 (2021) @dsmiley suggested LatLonDocValuesField.newDistanceSort for geospatial distance sorting "or PointVectorStrategy for general 2D".

Why it fails: the javadoc sentence describes the implementation, not a precondition for callers, and the code already departs from a plain pair of ranges for crossing boxes. LatLonPoint and LatLonShape are core APIs with their own types and oracle, while spatial-extras defines its predicates through spatial4j and says "-180 == 180" in its own code and tests. Canonicalizing the stored points alone cannot work: one storage sign is always missed by one of the two hemisphere boxes, and spatial4j turns the caller's rect(180, -170) into minX = -180; callers can avoid the miss by rewriting every query box that touches the dateline (the workaround under Suggested fix), which no documentation asks for. The #10728 remark is about distance sorting: no javadoc limits the strategy to non-geo contexts, and LUCENE-4389 ("Fix TwoDoubles dateline support") added dateline handling to this very method.

Suggested fix

Recommended Also match the other-sign meridian for a geo box that touches the dateline

In makeWithin, when a geo box that does not cross the dateline has an edge on ±180, also match points stored at the other-sign meridian: one exact-value clause, +6 lines, like BBoxStrategy's "-180 == 180" case. The repro passes with assertions on and off, and all spatial-extras tests pass.

       bq.setMinimumNumberShouldMatch(1); // must match at least one of the SHOULD
+    } else if (ctx.isGeo() && (bbox.getMinX() == -180 || bbox.getMaxX() == 180)) {
+      // touches dateline; -180 == 180, so also match points stored with the other sign
+      double otherX = bbox.getMinX() == -180 ? 180 : -180;
+      bq.add(rangeQuery(fieldNameX, bbox.getMinX(), bbox.getMaxX()), BooleanClause.Occur.SHOULD);
+      bq.add(rangeQuery(fieldNameX, otherX, otherX), BooleanClause.Occur.SHOULD);
+      bq.setMinimumNumberShouldMatch(1); // must match at least one of the SHOULD
     } else {
       bq.add(rangeQuery(fieldNameX, bbox.getMinX(), bbox.getMaxX()), MUST);
     }
All options and trade-offs4 options · 725 words

Option A was applied in a scratch worktree at a9779ef3c193 and run through gradle (seed DEADBEEF), and checked against every hunt repro with the patched class shadowed onto the main jars; the other options were not run. Workaround until a fix ships (executed on main): OR the box's query with a second PointVectorStrategy query for the degenerate box on the other-sign meridian, for example rect(180, 180, minY, maxY) next to rect(-180, 0, minY, maxY); the western hemisphere then finds (180, 10) and still misses (179.9, 10).

Also match the other-sign meridian for a geo box that touches the dateline (validated by the checks) (recommended)

A new branch in makeWithin between the crossing branch and the plain range: if ctx.isGeo() and the box has minX == -180 or maxX == 180, the x filter becomes two SHOULD clauses with setMinimumNumberShouldMatch(1), the plain range and an exact match on the opposite-sign meridian (+180 for a west edge at -180, -180 for an east edge at +180). It mirrors the crossing branch above it and BBoxStrategy's special case; the closest precedent is BBoxStrategy.makeWithin, which for the same boxes ORs in an exact match on the opposite dateline edge (BBoxStrategy.java:550-559).

       bq.setMinimumNumberShouldMatch(1); // must match at least one of the SHOULD
+    } else if (ctx.isGeo() && (bbox.getMinX() == -180 || bbox.getMaxX() == 180)) {
+      // touches dateline; -180 == 180, so also match points stored with the other sign
+      double otherX = bbox.getMinX() == -180 ? 180 : -180;
+      bq.add(rangeQuery(fieldNameX, bbox.getMinX(), bbox.getMaxX()), BooleanClause.Occur.SHOULD);
+      bq.add(rangeQuery(fieldNameX, otherX, otherX), BooleanClause.Occur.SHOULD);
+      bq.setMinimumNumberShouldMatch(1); // must match at least one of the SHOULD
     } else {
       bq.add(rangeQuery(fieldNameX, bbox.getMinX(), bbox.getMaxX()), MUST);
     }

Trade-offs

  • Behaviour change: only boxes with an edge exactly on ±180 change, and they now also return opposite-sign points on that edge. For the world box the extra clause is redundant but harmless.
  • Circles share makeWithin, so their bounding-box prefilter may now admit a point at the other-sign meridian; the distance check still decides, and the probed circles give the same answers as before.
  • Cost: one more single-value range clause for boxes that touch the dateline; no benchmark was run. No index format or API change, and existing indexes need no rebuild.

What the checks validated

  • Stock main a9779ef3c193 (gradle, scratch worktree, seed DEADBEEF): the repro fails 3 of 4 with assertions on and off; every upstream class that uses PointVectorStrategy (TestPointVectorStrategy, TestPortedSolr3, TestDistanceStrategy, TestQueryEqualsHashCode: 22 tests) passes; all of :lucene:spatial-extras:test (27 classes, 121 tests, 5 skipped) has only the repro's 3 failures.
  • With the fix: the repro passes 4 of 4 with assertions on and off; the 22 closest-suite tests pass; all of :lucene:spatial-extras:test passes with 0 failures, with the same per-class counts as stock except the repro class; ecjLintMain, ecjLintTest and checkGoogleJavaFormat pass; the diff applies cleanly to a clean tree.
  • Classpath runs with the patched class shadowed onto the main jars: this repro 4 of 4 (-ea, -da), the hunt's JUnit 2 of 2 and the independent reproducer's 4 of 4; the exhaustive grid drops from 420 to 0 mismatches per operation. The hunt's variant of the same fix also passed the all-strategies JUnit, the devil's-advocate probe (12 to 0) and the security probe (0 extra hits against the exact reference, 0 misses at ±180).

Route a touching box through the existing crossing branch (variant of A)

Treat a geo box with minX == -180 like a crossing box with ranges [-inf, maxX] or [180, +inf], and one with maxX == 180 like [-inf, -180] or [minX, +inf], reusing the two-SHOULD code. Not run.

Trade-offs

Same results, since no data lies beyond the world bounds (as the existing comment notes); a less obvious diff.

Store +180 as -180 at index time

Normalize x in createIndexableFields. Not recommended; not run.

Trade-offs

Does not fix boxes ending at +180 (the eastern hemisphere would then miss every antimeridian point), so the query side still needs A; existing indexes would have to be rebuilt.

Document that -180 and +180 are different values

A javadoc sentence. Not recommended.

Trade-offs

Keeps the wrong results, contradicts SpatialOperation's definition, and to get correct results callers would have to rewrite every query box that touches the dateline (the workaround above), since spatial4j itself writes edges with both signs.

Downstream note

Downstream: an application that uses a PointVectorStrategy box with an edge on ±180 as a MUST_NOT exclusion does not exclude opposite-sign points exactly on that edge; points off the edge, and the one crossing exclusion box tested, are handled correctly.

How it was found

Bounded-exhaustive comparison of the real spatial-extras jar with spatial4j's relate and an exact reference: every one of 570 grid rectangles against 35 grid points, Intersects and IsWithin. PointVectorStrategy alone missed 420 pairs per operation, all at ±180 with the other sign; code reading found the cause, and a JUnit confirmed it.

The method in full167 words

Method: semi-formal, bounded-exhaustive differential checking (hunt unit on spatial-extras, 2026-10-26, main ea57360912ed).

  • A harness enumerated every distinct geo rectangle over longitudes {-180, -170, -90, 0, 90, 170, 180} and latitudes {-90, -45, 0, 45, 90} (570 rectangles, crossing and degenerate ones included) and queried each against all 35 grid points with the exact strategies of the module.
  • Each answer was compared with two oracles: SpatialOperation.evaluate (spatial4j relate, the module's own test oracle) and an independent exact reference (closed intervals, -180 == 180).
  • PointVectorStrategy showed 420 false negatives per operation out of 19,950 pairs, every one a point at x = ±180 against a non-crossing box edge of the other sign, and 0 other mismatches; spatial4j and the reference agreed on all of them.
  • The finder read makeWithin, found the plain x range, wrote a public-API JUnit on the stock jar and a fix sketch that brought the grid to 0 mismatches. Re-run on main a9779ef3c193 for this doc: 420 stock, 0 with the fix.

Prior reports, history and checks

Prior reports

No prior report found (searched 2026-10-26, rechecked 2026-10-28): 144 recorded searches and checks over GitHub, the Lucene and Solr mailing lists, JIRA and the web, plus the files of all 411 open Lucene PRs. Closest: #9767, the same kind of defect in core LatLonShape, and LUCENE-5714 (#6776), which fixed these edges in BBoxStrategy only.

Searches6 searches

No issue, PR, JIRA entry, list thread, commit or web page reports this bug under the class's current name, its old name TwoDoublesStrategy, or Solr's wrapper type. Related, not the same bug: #9767 / LUCENE-8721 (open; core LatLonShape polygon and line queries; a committer there called the opposite-sign behaviour "more like a feature"), #6776 / LUCENE-5714 (precedent in BBoxStrategy), #5455 / LUCENE-4389 (origin of today's makeWithin), locationtech/spatial4j#85 (relate treats ±180 as equal), #9568 / LUCENE-8522 (a committer: longitude 180 and -180 "are in fact the same point"), #10728 / LUCENE-9689 (a suggestion to use LatLonDocValuesField.newDistanceSort for geospatial distance sorting, "or PointVectorStrategy for general 2D"), #8240 / LUCENE-7185 (random geo tests rarely hit ±180), #13305 (an undiagnosed prefix-tree test failure near ±180), SOLR-10729 / SOLR-13138 (Solr's wrapper deprecated and removed). Not fixed upstream: the last commit touching the file (2026-04-01) is a refactor, and the only open PR touching it, #12295, changes casts only. Limits: keyword search only; the old Lucene.NET JIRA was not searched; private security reports are not visible.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues and PRs (apache/lucene, apache/solr, Elasticsearch, OpenSearch, Lucene.NET, spatial4j, all of GitHub)2026-10-26a scripted prior-report search (16 issue/PR, 3 commit, 2 downstream queries) plus 57 read-only searches: old and new class names, "-180 == 180", "touches dateline", "world wrap", JIRA keys, module labelsno report of this defect; #9767 (closest, core LatLonShape), #6776 (precedent), #5455 (origin)
Open apache/lucene PRs2026-10-26snapshot of all 411 open PRs and the files they touchonly #12295 touches PointVectorStrategy.java (casts only)
Mailing lists2026-10-2620 tool queries over dev@, java-user@, issues@ and commits@ plus 15 direct searches (Lucene lists, Solr user lists)usage questions only; none mentions ±180
JIRA archive (LUCENE, SOLR)2026-10-264 tool queries and 6 JQL searches (PointVectorStrategy, TwoDoublesStrategy, SpatialPointVectorFieldType with dateline and 180)nothing on this; SOLR-10729 / SOLR-13138 (deprecation, removal)
Web2026-10-2615 searchesunrelated pages
GitHub re-check2026-10-28issue and PR searches "PointVectorStrategy 180", "PointVectorStrategy dateline", "antimeridian spatial-extras", "PointVectorStrategy antimeridian", "TwoDoublesStrategy dateline"; the file's upstream commit list0 relevant hits ("PointVectorStrategy 180" matches only the unrelated PR #180 by its number); file unchanged since 2026-04-01

History

Introduced in 4.0.0 by LUCENE-4351 (2012-09-05), which upgraded spatial4j from 0.2, which turned +180 into -180, to 0.3, which keeps +180; the x filter itself was unchanged. Executed on 14 releases and main: affected from 4.0.0 to 10.5.2; the 4.0.0 pre-releases and the parent commit have no silent miss (one mirror case throws there instead).

Versions tested7 rows

Introduced by LUCENE-4351 (issue #5417, "Upgrade Spatial4j to 0.3"; commit message "Update to Spatial4j 0.3"), branch_4x 6f1fde90512 and trunk 1db287ce640, 2012-09-05, by @dsmiley (SVN and JIRA-patch era, no pull request). It changed no line of the x filter. spatial4j 0.2 normalized +180 to -180 when it built a point or a box, and its relate treated the two as different, so the plain range agreed with the oracle; from 0.3 on, +180 is kept and relate treats both signs as one meridian. The parent 8dd92aa7e90 has no silent miss (the mirror case throws "Crossing dateline not yet supported" there, as in 4.0.0-ALPHA/BETA); 6f1fde90512 misses 2 of 2 cases (commit builds). Releases between the 14 executed ones were not run; git log --follow shows no change to the x logic after LUCENE-4389.

Code origin. The plain x range dates from the module import, 66a852234d4 (LUCENE-3795, 2012-02-20). Today's crossing/non-crossing makeWithin is LUCENE-4389 "Fix TwoDoubles dateline support" (#5455; trunk 922c7316d81, branch_4x ed444bdac7d, 2012-09-15), which handled only boxes that cross. The class was called TwoDoublesStrategy then. branch_10_6 and branch_10x have the same makeWithin; the last upstream commit touching the file is dfd536186fe (2026-04-01, an instanceof refactor).

Versions and branches executed, with results
Version or commitVerdictResult
main a9779ef3c193 affectedthis repro 3 of 4 fail with -ea and -da (gradle and classpath runner); the hunt's JUnit 2 of 2
10.5.2, 9.12.3 affectedthis repro 3 of 4 fail (-ea; 10.5.2 also -da); the hunt's JUnit 2 of 2 fail with -ea and -da; the reproducer's JUnit 2 of 4 fail (-ea)
6.0.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 10.0.0 affectedthe hunt's JUnit 2 of 2 fail (pre-9.1 test-framework import where needed)
4.0.0, 4.10.4, 5.0.0, 5.5.5 main() portsboth cases missed, the same-sign control found (4.0.0 also with -da)
4.0.0-ALPHA, 4.0.0-BETA no miss; throwsspatial4j 0.2: the point is normalized to -180 and found; the mirror box rect(170, 180) is built as crossing and the query throws UnsupportedOperationException ("Crossing dateline not yet supported")
6f1fde90512 (LUCENE-4351) vs its parent 8dd92aa7e90 commit buildsthe commit misses 2 of 2 cases; its parent misses none: it finds the first, and for the second spatial4j 0.2 builds a crossing box and the strategy throws "Crossing dateline not yet supported", as in 4.0.0-ALPHA/BETA
3.6.2 no APIno spatial vector or query packages (compile error)

Independent checks

An independent reproducer (own JUnit fails 2 of 4, controls pass), an intended-behaviour devil's advocate (real; five arguments rejected), read-only cross-model reviews (root cause agreed), an adjudication (confirmed, high confidence), a credibility audit, re-runs on main a9779ef3c193 for this doc, and fact checks of this doc (their corrections applied).

Checks and verdicts492 words

Executed checks ran on main (and 10.5.2 and 9.12.3 where stated), with assertions on and off. The cross-model reviewer could only read code, so its verdicts on reality are procedural.

Each independent check and its verdict
CheckVerdictWhat it found
Independent reproducer realOwn public-API JUnit from the claim alone: 2 of 4 fail on main (-ea, -da), 10.5.2 and 9.12.3, and 20 of 40 over 10 random iterations (always the two bug tests); the same-sign and BBoxStrategy controls pass; 4 of 4 with the fix. Found no circle miss.
Cross-model review, works-as-designed lens (OpenAI GPT-5.6, read-only) real (medium)Intended argument weak, root cause agrees, inputs in contract; a separate re-run of the finder's JUnit fails 2 of 2, and an all-strategies JUnit fails only for PointVectorStrategy.
Cross-model review, refute lens (OpenAI GPT-5.6, read-only) not real (low), procedural onlyCould not run code; root cause agrees, inputs in contract, intended argument weak: "not a persuasive substantive refutation".
Intended-behaviour devil's advocate real (minor severity)Five intended-behaviour arguments tried, none survives; its probe finds 12 PointVectorStrategy mismatches on main, 10.5.2 and 9.12.3 (including both hemisphere boxes and rect(180, -170)), 0 with the fix, while BBoxStrategy and both prefix trees return the hit on every bug input (the quad tree, an approximate grid, also matches the outside control at 179.9); 22 upstream tests pass with and without the fix. Asked to drop the circle claim.
Adjudication confirmed (high)Re-ran the independent reproduction's JUnit (2 of 4 fail, -ea and -da), the finder's (2 of 2) and the independent reproduction's with the fix (4 of 4 pass); limited the claim to rectangle queries.
Credibility audit keepIts own probe fails exactly the hemisphere boxes and the caller's rect(180, -170) on main (-ea, -da), while same-sign and crossing controls pass.
Re-runs on main a9779ef3c193 still reproducesThe finder's and the independent reproduction's JUnits fail exactly as on ea57360912ed; this doc's JUnit and the exhaustive grid were also run there, stock and with the fix.
Independent fact check of this doc bug confirmed; 2 errors correctedRe-ran the gradle repro, the module suite stock and with the fix, lint and format, the repro harness (main -ea/-da, 10.5.2 -da), the exhaustive grid, the cross-model review's all-strategies JUnit and a probe of every geo-point strategy class on a9779ef3c193; all matched. Found two errors, both fixed here: a test citation (TestHeatmapFacetCounter) that did not show what was claimed, and the parent commit's result (one case throws, none found missing).
Cross-model fact check (OpenAI GPT-5.6, read-only) bug real; corrections appliedBug real and well reproduced, fix sound for rectangles. Its corrections are applied here: the 9,000,000-check probe's 0 extra hits are against an exact reference (spatial4j relate disagrees on 96 rounding cases), the #10728 advice, which strategies each probe covered, where the fix was validated, and softer wording for index size, release coverage, workarounds and novelty.

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

Finding record

LFV-301, snapshot of 2026-11-06

Registry title
PointVectorStrategy misses a point at longitude +180 or -180 when a rectangle query that does not cross the dateline has its edge on that meridian with the other sign (-180 vs +180)
Area
spatial-extras / PointVectorStrategy (Intersects and IsWithin rectangle queries: makeWithin x filter)
Priority
Low, rank 423: The experimental PointVectorStrategy silently misses points that lie exactly on the antimeridian when a box edge has the opposite sign, a boundary-value geo miss that is rarer than LFV-316.
Impact class
silent wrong results
Impact note
Applications that index points with the @lucene.experimental PointVectorStrategy in its default geo context (SpatialContext.GEO) and run Intersects or IsWithin rectangle queries silently lose a document whose longitude is exactly +180 or -180 when the query box's edge on that meridian is written with the other sign (for example the western-hemisphere box rect(-180,0,-90,90) misses Pt(180,10)), at any index size and with default IndexWriterConfig, codec and searcher; released since 4.0.0; only missed matches, never extra ones, and circle queries are not shown to be affected.
Summary
You index a point with PointVectorStrategy in the default geo context and search it with SpatialOperation.Intersects or IsWithin and a rectangle that does not cross the dateline but has one edge on the antimeridian, expecting the point to be found whenever spatial4j's relate, which defines these predicates, says it is inside (in geo, -180 and +180 are the same meridian). Instead a point stored at x=+180 is missed by a box whose west edge is -180 (Pt(180,0) with rect(-180,-170,-10,10): 0 hits, oracle 1), and a point at x=-180 by a box whose east edge is +180 (Pt(-180,0) with rect(170,180,-10,10)). BBoxStrategy and both prefix-tree strategies in the same module return the hit; PointVectorStrategy.makeWithin filters x with one plain DoublePoint range [minX, maxX] for every non-crossing box, so +180 and -180 are different values to it.
Introduced
6f1fde90512 (branch_4x, svn r1381280; trunk equivalent 1db287ce640, svn r1381230), 'LUCENE-4351 Update to Spatial4j 0.3' (issue apache/lucene#5417), by dsmiley, no PR (SVN + JIRA patch era) [exec: the parent commit 8dd92aa7e90 is not affected, 6f1fde90512 misses 2 of 2 cases]. Behavioural introduction: the commit changed no line of the x filter; it bumped spatial4j 0.2 to 0.3. spatial4j 0.2 normalized longitude +180 to -180 in makePoint/makeRect and its relate treated the two values as different, so the plain range agreed with the oracle; from 0.3 on, +180 is kept and relate treats +/-180 as one meridian. Code origin [read, git blame]: the plain x range dates from the module import 66a852234d4 (LUCENE-3795, 2012-02-20); the current crossing/non-crossing shape of makeWithin is from LUCENE-4389 'Fix TwoDoubles dateline support' (#5455; trunk 922c7316d81, branch_4x ed444bdac7d, 2012-09-15), which handled only boxes that cross the dateline (2012-09-05 (6f1fde90512 on branch_4x and 1db287ce640 on trunk))
First affected version
4.0.0 [exec] (tag 2012-10-11); the earlier builds that shipped the strategy, 4.0.0-ALPHA and 4.0.0-BETA (spatial4j 0.2), are not affected [exec], and 3.6.x has no such API [exec: compile error on 3.6.2]
Affected versions
Every release from 4.0.0 on, and main. [exec] fails on 4.0.0 (-ea and -da), 4.10.4, 5.0.0, 5.5.5, 6.0.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3 (-ea and -da), 10.0.0, 10.5.2 (-ea and -da) and main ea57360912ed (-ea and -da), with spatial4j 0.3 to 0.8 (follow-up; 4.x and 5.x through main() ports of the same cases and oracle); the independent reproduction, the cross-model review and the intended-behaviour check independently reproduce on main, 10.5.2 and/or 9.12.3. [read] branch_10_6 and branch_10x have the same makeWithin; upstream main is unchanged (last commit touching PointVectorStrategy.java is dfd536186fe, 2026-04-01, cosmetic). Not affected: 4.0.0-ALPHA, 4.0.0-BETA and branch_4x 8dd92aa7e90 [exec]; 3.6.2 has no such API [exec].
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
semi-formal: exhaustive check
Upstream and prior reports
none found (2026-10-26, the prior-report record, verdict new: 144 queries in total: a scripted prior-report search (16 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list over dev@, java-user@, issues@ and commits@, 4 JIRA, 6 web), 57 read-only GitHub GraphQL searches (apache/lucene, apache/solr, Elasticsearch, OpenSearch, Lucene.NET, spatial4j, all of GitHub), 6 JIRA searches over LUCENE and SOLR, 15 direct mailing-list searches, 9 web searches, a snapshot of all 411 open apache/lucene PRs with their files, and the local git history; plus the finder's, the independent reproduction's and the intended-behaviour check's earlier searches); not fixed upstream (makeWithin unchanged on main; the only open PR touching PointVectorStrategy.java, #12295, changes casts only). Related items, not reports of this bug: #9767 / LUCENE-8721 (open since 2019; the same defect class in core LatLonShape polygon and line queries, different module and fix site; a committer there called the opposite-sign behaviour 'more like a feature'), #6776 / LUCENE-5714 (fixed the matching dateline-edge bugs in BBoxStrategy only, 2014), #5455 / LUCENE-4389 (origin of today's makeWithin crossing branch), locationtech/spatial4j#85 (relate deems +/-180 equal), #9568 / LUCENE-8522 (a committer: longitude 180 and -180 'are in fact the same point'), #13305 (an undiagnosed RPT test failure near +/-180), LUCENE-9154 (core LatLonPoint special-cases 180), #10728 / LUCENE-9689, #8240 / LUCENE-7185, SOLR-10729 / SOLR-13138 (Solr's PointVector field type deprecated in 6.6 and removed in 9.0)
Lucene commit tested
main a9779ef3c193 (commit of 2026-10-26): this doc's JUnit through gradle in a clean scratch worktree, stock and with the fix, assertions on and off, plus all of lucene/spatial-extras (121 tests) stock and with the fix and the module's lint and format checks (2026-10-28); through the hunt's classpath runner, the same JUnit on main (-ea, -da, fix shadowed), 10.5.2 and 9.12.3, and the exhaustive grid stock and with the fix. An independent fact check (2026-10-28) re-ran the gradle repro, the module suite stock and with the fix, the classpath runner on main and on 10.5.2 with -da, the cross-model review's all-strategies JUnit and a probe of every geo-point strategy class on a9779ef3c193. Hunt runs on main ea57360912ed (identical lucene/spatial-extras), on 14 releases from 4.0.0 to 10.5.2, the two 4.0.0 pre-releases and four commit builds. OpenJDK 25.0.3, Linux aarch64.

Lucene and upstream sources

Finding record
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/vector/PointVectorStrategy.java:54-81 (class javadoc: 'Can query by a rectangle or circle', Intersects and IsWithin supported, 'a pair of range queries on two DoublePoints', @lucene.experimental at :80), :209-222 (makeWithin: two SHOULD ranges only when getCrossesDateLine(), else one plain range [minX, maxX] on x)
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/query/SpatialOperation.java:29-35 (predicates defined by evaluate(); 'makes no distinction of shape boundaries')
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java:387,395 ('touches dateline; -180 == 180')
  • lucene/spatial-extras/src/test/org/apache/lucene/spatial/bbox/TestBBoxStrategy.java:108-175 (testIntersectsBugDatelineEdge, testWithinBugDatelineEdge, testContainsBugDatelineEdge, testAlongDatelineOppositeSign) and src/test/org/apache/lucene/spatial/prefix/TestRandomSpatialOpFuzzyPrefixTree.java:518 ('in geo, -180 & +180 are the same place')
The finding in 30 seconds
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/vector/PointVectorStrategy.java:54-81 (at a9779ef3c193) (class javadoc), :163-180 (indexed fields), :187-206 (makeQuery), :209-222 (makeWithin), :228-242 (rangeQuery)
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/query/SpatialOperation.java:29-35 (at a9779ef3c193) (predicates defined by evaluate)
Who is affected
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/vector/PointVectorStrategy.java:80 (at a9779ef3c193) (@lucene.experimental), :116-118 (newInstance, DEFAULT_FIELDTYPE), :228-242 (rangeQuery; throws without point values at :241)
Why it looks unintended
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/vector/PointVectorStrategy.java:62-64, (at a9779ef3c193) :72-73, :80
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/SpatialStrategy.java:116-117 (at a9779ef3c193)
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/query/SpatialOperation.java:30-34 (at a9779ef3c193)
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java:387, (at a9779ef3c193) :395
  • lucene/spatial-extras/src/test/org/apache/lucene/spatial/bbox/TestBBoxStrategy.java:108-175 (at a9779ef3c193)
  • lucene/spatial-extras/src/test/org/apache/lucene/spatial/prefix/TestRandomSpatialOpFuzzyPrefixTree.java:518-519 (at a9779ef3c193)
Suggested fix
  • lucene/spatial-extras/src/java/org/apache/lucene/spatial/vector/PointVectorStrategy.java:212-216 (at a9779ef3c193) (crossing branch and its comment); lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java:550-559 (makeWithin: exact match on the opposite dateline edge)

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

Repro files

About the diagrams

Made in parallel with this doc; its values come from an executed probe of the same example (the western hemisphere rect(-180, 0, -90, 90) and the point (180, 10)) on main a9779ef3c193, with assertions on and off and with the fix. is the summary card (the PNG).

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.