All findings · Lucene finding · LFV-570 · wkt-multipoint-parenthesized-points-rejected

SimpleWKTShapeParser rejects MULTIPOINT ((1 2), (3 4)), the standard WKT form with each point in its own parentheses

SimpleWKTShapeParser.parse("MULTIPOINT ((1 2), (3 4))") throws ParseException: expected ) but found: ',', although the same parser reads MULTIPOINT ((1 2)) and MULTIPOINT (1 2, 3 4). It never reads the first point's ), so it ends the list one token early; every release from 7.6.0 to 10.6.0 has it.

Summary

Priority
Low, rank 1070The public core SimpleWKTShapeParser loudly rejects the standard parenthesized MULTIPOINT form with an easy workaround and known downstream users.
Impact
wrong error
Since
7.6.0 (LUCENE-8538, bd714ca1edc and backport 5e7858041b8, 2018-10-24)introduced 2018-10-24 · about 8 years
Affected
7.6.0-10.6.0 and main
Status
documented
Found by
non-formal: differential parsing probe, then a public-API JUnit test

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

One missing read of ): the parser handles a parenthesized point in the middle of a list, but not the first one (unless it is alone) or the last one.

Background: how WKT writes a multipoint

  1. WKT (well-known text) writes a geometry as text: POINT (1 2) is one point, LINESTRING (1 2, 3 4) a line through two points.
  2. A multipoint is a set of points. The Simple Features grammar writes each member as its own point text in parentheses, MULTIPOINT ((1 2), (3 4)); many tools also accept the older bare form MULTIPOINT (1 2, 3 4).
  3. Lucene's SimpleWKTShapeParser.parse turns WKT into Lucene shapes; a multipoint becomes a double[][] with one {x, y} row per point.
Diagram of the bug: the WKT text MULTIPOINT ((1 2), (3 4)) split into tokens. The parser reads the list's opening parenthesis, the first point's opening parenthesis and the numbers 1 and 2, but not the point's closing parenthesis; its loop then reads that closing parenthesis as the end of the whole list, and the check after the loop finds the comma and throws ParseException: expected ) but found: ','. Expected result: {{1, 2}, {3, 4}}. MULTIPOINT ((1 2)) and MULTIPOINT (1 2, 3 4) parse.
The tokens of MULTIPOINT ((1 2), (3 4)) as the parser reads them: the first point's ) is taken as the end of the list, so the next , arrives where a ) is expected. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Parse a two-point multipoint written the standard WKT way, each point in its own parentheses: SimpleWKTShapeParser.parse("MULTIPOINT ((1 2), (3 4))").
  2. You expect double[][] {{1, 2}, {3, 4}}. The same parser returns exactly that for the bare form MULTIPOINT (1 2, 3 4), and {{1, 2}} for MULTIPOINT ((1 2)).
  3. Instead it throws java.text.ParseException: expected ) but found: ','. It reads the first point's (, 1 and 2, but not its ). It takes that ) as the end of the whole list, then finds , where it still wants the point's ).
  4. Position decides: MULTIPOINT (1 2, (3 4), 5 6) parses, but MULTIPOINT (1 2, (3 4)) fails with expected word but found: END-OF-STREAM. 3D points and a GEOMETRYCOLLECTION that holds such a multipoint fail too. No wrong shape is ever returned.

Who is affected

Applications calling the public parse or parseExpectedType on multipoints from writers that use the standard form, such as PostGIS's ISO WKT writer since 2022. The geometry, or the collection holding it, is refused loudly: no wrong shape, no hang, no setting involved. In Lucene only tests call the parser; the bare form is a workaround.

Mechanism, terms and numbers706 words

SimpleWKTShapeParser, the public WKT reader in core org.apache.lucene.geo, says it parses WKT and complies with ISO/IEC 13249-3:2016. In the Simple Features WKT grammar each member of a multipoint is a point text in its own parentheses, MULTIPOINT ((10 10), (20 20)); the bare MULTIPOINT (10 10, 20 20) is the older, widely accepted variant. The parser accepts the bare form and a single parenthesized point, but MULTIPOINT ((1 2), (3 4)) throws java.text.ParseException: expected ) but found: ','. In parseCoordinates the code before the loop reads a parenthesized first point's ( and numbers but not its ); the loop condition then reads that ) as the end of the whole list, and the check after the loop finds the ,. Points read inside the loop get their ) checked at once, so acceptance depends on position: MULTIPOINT (1 2, (3 4), 5 6) parses, MULTIPOINT (1 2, (3 4)) fails with expected word but found: END-OF-STREAM. The failure is loud and never yields a wrong shape. It dates from the class's first commit (LUCENE-8538, 2018) and is in every release from 7.6.0 to 10.6.0 and on main. Reading each multipoint member as bare or ( point ) fixes it.

Terms

  • SimpleWKTShapeParser: a public class in core org.apache.lucene.geo (exported, no internal or experimental tag) with two entry points, parse(String) and parseExpectedType(String, ShapeType), both declaring a checked java.text.ParseException (SimpleWKTShapeParser.java:50-55 at 645d4fe14a20).
  • parseCoordinates (:122-145): the shared helper that reads a comma-separated list of coordinates up to its closing ). Multipoints, lines, polygon rings and holes all use it.
  • nextCloserOrComma (:374-383): reads one token and returns it if it is , or ). The loop over the list continues on , and stops on ).

The bug, token by token

For MULTIPOINT ((1 2), (3 4)), after parseMultiPoint has read the list's ( (:161) and called parseCoordinates (:167):

  1. :126: the next token is (, not a number, so nextWord reads it and sets isOpenParen = true; parseCoordinate reads 1 and 2 (:127). The point's ) is not read.
  2. :130: nextCloserOrComma reads that ), the first point's closer, so the loop ends at once: the parser now believes the whole list is closed.
  3. :141: isOpenParen is still true, so nextCloser reads the next token, ,, and throws ParseException("expected ) but found: ','") (:351-352).

MULTIPOINT ((1 2)) passes by coincidence: the point's ) ends the loop and the list's ) satisfies the check. A parenthesized point read inside the loop has its ) checked at once (:135-138), and isOpenParen is reset each iteration (:131), so a mid-list one works. A parenthesized last point leaves isOpenParen set after the list's ) was read, so :141 asks for one more ) and hits the end of input: expected word but found: END-OF-STREAM (:294).

Polygon holes rely on the first-position branch: parsePolygonHole (:203-210) calls parseCoordinates with the hole's own ( unread, :126 swallows it, and :131 clears the flag at the hole's second coordinate. With the check after the loop, holes still parse; a fix that checks the first point's ) early must therefore also make parsePolygonHole read its own (.

The numbers (main, assertions on and off)

inputresult
MULTIPOINT (1 2, 3 4){{1, 2}, {3, 4}}
MULTIPOINT ((1 2)){{1, 2}}
MULTIPOINT ((1 2), (3 4)), MULTIPOINT((1 2),(3 4))ParseException: expected ) but found: ','
MULTIPOINT ((1 2 9), (3 4 9)) (3D)same
MULTIPOINT ((1 2), 3 4)same
MULTIPOINT (1 2, (3 4))ParseException: expected word but found: END-OF-STREAM
MULTIPOINT (1 2, (3 4), 5 6)parses
GEOMETRYCOLLECTION (MULTIPOINT ((1 2), (3 4)), POINT (5 6))expected ) but found: ','
LINESTRING ((1 2), (3 4)) (not standard WKT; same helper)expected ) but found: ','; LINESTRING (1 2, (3 4), 5 6) parses
a polygon with a hole, a MULTILINESTRINGparse (not affected)

Side note: the helper's own messages "expected: [)] but found: [...]" (:136-137, :142-143) are never produced, because nextCloser throws first for anything other than ) (:347-353).

Exposure and likelihood271 words
  • Who is hit. Callers of SimpleWKTShapeParser.parse or parseExpectedType (public static, checked ParseException) with a standard-form multipoint of two or more points, alone or inside a GEOMETRYCOLLECTION. The class is in an exported core package (module-info.java:38) with no internal or experimental tag. In the Lucene tree only tests call it (TestSimpleWKTShapeParsing, TestTessellator, TestShapeDocValues).
  • Downstream users (read only, nothing run): rdf4j's Lucene index catches the ParseException, logs a warning and adds no geo fields, so such a geometry is left out of its geo index and geo queries miss that record; a copy of that class lives in INCEpTION; Anserini's generator does not handle MULTIPOINT anyway. No use was found in Elasticsearch, OpenSearch or Solr (code search, not exhaustive).
  • Who writes the standard form. PostGIS's ISO WKT writer since PR #682 ("MultiPoint WKT writer: use parentheses in sub-members in WKT_ISO format", merged 2022-04-12). Elasticsearch #151876 (open) says JTS writes MULTIPOINT ((0 0),(1 1), ...) and that it "can happen in the wild"; Elasticsearch #138162 (open) asks for the same form.
  • Default, no setting. The parser has no options; assertions on or off make no difference.
  • What happens. The first such multipoint is refused with a checked ParseException: no wrong or partial shape, no hang (a 200,000-point standard-form input fails in 0.2 ms, at the first ,), nothing written.
  • Scale and likelihood. Two points are enough. Multipoints are a minority geometry type and no user report turned up in the recorded searches, so it is rated plausible, low priority.
  • Workaround. Rewrite to the bare form, MULTIPOINT (1 2, 3 4), before parsing.

Minimal repro

One JUnit test on the public API: parse MULTIPOINT ((1 2), (3 4)) and compare the result with {{1, 2}, {3, 4}}. Deterministic, single thread, about a second.

TestWKTMultiPointParenthesizedPoints.java (JUnit, one 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.geo;

import java.text.ParseException;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestWKTMultiPointParenthesizedPoints extends LuceneTestCase {

  public void testTwoParenthesizedPoints() throws Exception {
    String wkt = "MULTIPOINT ((1 2), (3 4))";
    Object shape;
    try {
      shape = SimpleWKTShapeParser.parse(wkt);
    } catch (ParseException e) {
      throw new AssertionError("expected " + wkt + " to parse as {{1, 2}, {3, 4}}, got " + e, e);
    }
    assertArrayEquals(new double[][] {{1, 2}, {3, 4}}, (double[][]) shape);
  }
}
Command
cp TestWKTMultiPointParenthesizedPoints.java $LUCENE/lucene/core/src/test/org/apache/lucene/geo/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.geo.TestWKTMultiPointParenthesizedPoints -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
Expected
MULTIPOINT ((1 2), (3 4)) parses to double[][] {{1, 2}, {3, 4}}.
Observed
java.text.ParseException: expected ) but found: ',' from parseCoordinates (line 141), with assertions on and off.

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

More files, environment, full output209 words

Pure string parsing: no index, no randomness (the seed does not matter). The ParseException is rethrown as an AssertionError that states expected and actual, with the original exception as its cause. It compiles against 9.1.0 and later; before 9.1.0 (7.x, 8.x, 9.0.0) import org.apache.lucene.util.LuceneTestCase instead, and before 8.5.0 add the sandbox jar, where the class lived then.

As the regression test: the same test, for example as a new method in TestSimpleWKTShapeParsing.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); the same test fails on 10.6.0; the hunt's 4-test version fails on the 11 releases it was run on, 7.6.0 to 10.5.2 (the releases in between by reading)
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64 (pure string parsing; no platform dependence expected)
Command (environment)
cp $REPRO/TestWKTMultiPointParenthesizedPoints.java $LUCENE/lucene/core/src/test/org/apache/lucene/geo/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.geo.TestWKTMultiPointParenthesizedPoints -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false

Observed output

$ ./gradlew :lucene:core:test --tests org.apache.lucene.geo.TestWKTMultiPointParenthesizedPoints -Ptests.seed=DEADBEEF

TestWKTMultiPointParenthesizedPoints > testTwoParenthesizedPoints FAILED
    java.lang.AssertionError: expected MULTIPOINT ((1 2), (3 4)) to parse as {{1, 2}, {3, 4}}, got java.text.ParseException: expected ) but found: ','
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:76696EB2AFA5FE2B]:0)
        at org.apache.lucene.geo.TestWKTMultiPointParenthesizedPoints.testTwoParenthesizedPoints(TestWKTMultiPointParenthesizedPoints.java:30)

        Caused by:
        java.text.ParseException: expected ) but found: ','
            at org.apache.lucene.geo.SimpleWKTShapeParser.nextCloser(SimpleWKTShapeParser.java:352)
            at org.apache.lucene.geo.SimpleWKTShapeParser.parseCoordinates(SimpleWKTShapeParser.java:141)
            at org.apache.lucene.geo.SimpleWKTShapeParser.parseMultiPoint(SimpleWKTShapeParser.java:167)
            at org.apache.lucene.geo.SimpleWKTShapeParser.parseGeometry(SimpleWKTShapeParser.java:90)
            at org.apache.lucene.geo.SimpleWKTShapeParser.parseExpectedType(SimpleWKTShapeParser.java:69)
            at org.apache.lucene.geo.SimpleWKTShapeParser.parse(SimpleWKTShapeParser.java:51)
            at org.apache.lucene.geo.TestWKTMultiPointParenthesizedPoints.testTwoParenthesizedPoints(TestWKTMultiPointParenthesizedPoints.java:28)

ERROR: 1 test has failed:

  - org.apache.lucene.geo.TestWKTMultiPointParenthesizedPoints.testTwoParenthesizedPoints (:lucene:core)
BUILD FAILED

Why it looks unintended

  • Its branches for parenthesized points work for one point and mid-list: MULTIPOINT ((1 2)) and MULTIPOINT (1 2, (3 4), 5 6) parse.
  • It claims ISO/IEC 13249-3 compliance; that grammar writes each member as (x y): MULTIPOINT ((10 10), (20 20)).
  • The Elasticsearch parser it was ported from accepts this form; no Lucene test or doc rules it out.
The full argument304 words
  • Contract. Class javadoc (SimpleWKTShapeParser.java:29-34): "Parses shape geometry represented in WKT format", "complies with OGC® document: 12-063r5 and ISO/IEC 13249-3:2016 standard". The Simple Features grammar, read through the OGC's standalone WKT reference and secondary sources (the ISO text itself was not read), defines <multipoint text> as ( <point text> {, <point text>}* ) with <point text> ::= ( <point> ), example MULTIPOINT ((10 10), (20 20)); one transcription marks the bare form "non-standard but widely used". 12-063r5 is the WKT standard for coordinate reference systems, not geometry, so it is not the argument here.
  • Code intent. Dedicated ( branches (:126, :132) and ) checks (:135-138, :141-144); acceptance by position (one point yes, mid-list yes, first or last no) is not described as intended in any documentation or commit message found.
  • Origin. The class is a port of Elasticsearch's GeoWKTParser (same identifiers, same javadoc line). In Elasticsearch v6.4.0, parseCoordinateList checks the first point's ) right after it, before the loop, and reads polygon holes through a separate opener; the port checks it after the loop, which keeps parsePolygonHole working and breaks multipoints. Elasticsearch PR #27417 (2017), the ported code, lists MULTIPOINT ((10 40), (40 30), ...) as supported. The Lucene commit gives no reason.
  • Issue, docs, tests. #9584 (LUCENE-8538) lists MULTIPOINT -> double[][] with no syntax limit; CHANGES.txt (7.6.0, :7093-7094) reads "Add a Simple WKT Shape Parser for creating Lucene Geometries (Polygon, Line, Rectangle) from WKT format", naming those three types and no syntax limit; no javadoc, CHANGES.txt or MIGRATE.md entry rules the form out. TestSimpleWKTShapeParsing.testMultiPoint (:45-60) tests only the bare form. In-tree precedent (as the cross-model reviewer pointed out): TestGeo3d (:42) gives MULTIPOINT ((10 40), (40 30), (20 20), (30 10)) to spatial4j's WKT reader and expects it to parse.
The devil's-advocate check132 words

Strongest case for intended: the class is "Simple"; the introducing issue calls it "useful for simple tests, but also helps for benchmarking from real world data"; it has no in-tree production caller; it supports only a WKT subset (no Z/M tags, no TIN); its compliance claim links a CRS standard; its one MULTIPOINT test uses the bare form; and it fails loudly. So the parenthesized form would be an enhancement.

Why it fails: that covers features the parser never tried to support. Here it does try: explicit branches for parenthesized points that work for one point and in mid-list and fail only at the ends of a list, while the original it was ported from handles the form and the API is public and untagged. It limits severity (low priority), not validity.

Suggested fix

Recommended B. MULTIPOINT reads each point bare or in its own parentheses

Let parseMultiPoint read each point itself, bare or as ( point ), in one do/while loop (+10/-1 lines); lines and polygons stay untouched. With it the repro passes with assertions on and off, and all org.apache.lucene.geo tests plus TestShapeDocValues pass.

     ArrayList<Double> lats = new ArrayList<>();
     ArrayList<Double> lons = new ArrayList<>();
-    parseCoordinates(stream, lats, lons);
+    do {
+      // each point is written either bare, "1 2", or in its own parentheses, "(1 2)"
+      if (isNumberNext(stream)) {
+        parseCoordinate(stream, lats, lons);
+      } else {
+        nextOpener(stream);
+        parseCoordinate(stream, lats, lons);
+        nextCloser(stream);
+      }
+    } while (nextCloserOrComma(stream).equals(COMMA));
All options and trade-offs2 options · 546 words

Two options were run through gradle in a scratch worktree of 645d4fe14a20; B is recommended because it changes nothing outside MULTIPOINT. Moving only the first point's ) check before the loop, as in the Elasticsearch original, would break every polygon hole unless parsePolygonHole also reads its own (, which is option A.

B. MULTIPOINT reads each point bare or in its own parentheses (validated by the checks) (recommended)

In SimpleWKTShapeParser.parseMultiPoint (:159-173), replace the call to the shared parseCoordinates (:167) with its own loop.

     ArrayList<Double> lats = new ArrayList<>();
     ArrayList<Double> lons = new ArrayList<>();
-    parseCoordinates(stream, lats, lons);
+    do {
+      // each point is written either bare, "1 2", or in its own parentheses, "(1 2)"
+      if (isNumberNext(stream)) {
+        parseCoordinate(stream, lats, lons);
+      } else {
+        nextOpener(stream);
+        parseCoordinate(stream, lats, lons);
+        nextCloser(stream);
+      }
+    } while (nextCloserOrComma(stream).equals(COMMA));

Trade-offs

  • Accepts the standard form, the bare form and mixed lists (which the current in-loop branch already means to accept), with 3D points and inside collections.
  • LINESTRING, polygon and hole parsing are unchanged (checked: LINESTRING (1 2, (3 4), 5 6) still parses, LINESTRING ((1 2), (3 4)) is still refused; neither is standard WKT).
  • Rejects one malformed multipoint that main accepts, MULTIPOINT ((1 2, 3 4) (unbalanced; main reads two points), with expected ) but found: ','.
  • Leaves the shared helper's quirk in place for other types. Still one linear pass over the tokens.
  • Covers non-empty point texts only: point-level EMPTY members, legal in the grammar (MULTIPOINT ((1 2), EMPTY), MULTIPOINT (EMPTY)), are rejected on main and with A or B alike (invalid number found: EMPTY, because isNumberNext treats any word as a number); supporting them is a separate decision.

What the checks validated

Gradle, scratch worktree of 645d4fe14a20 (2026-11-01): the repro fails on the stock tree and passes with B, with assertions on and off; all 17 org.apache.lucene.geo test classes plus TestShapeDocValues pass (200 tests, 0 failures, 7 skipped, incl. TestSimpleWKTShapeParsing and TestTessellator); :lucene:core:check -x test passes. With B shadowed over the main jars, all 104,760 well-formed inputs of a 200,000-input differential probe parse to exactly the written shape.

A. Fix the shared coordinate reader (alternative) (validated by the checks)

parseCoordinates reads every coordinate through a new parseBareOrParenthesizedCoordinate (bare, or nextOpener + coordinate + nextCloser), and parsePolygonHole reads the hole's own ( first (the finder's sketch; +14/-16 lines).

Trade-offs

  • One rule for every coordinate list; holes no longer rely on the quirk.
  • Changes other types: LINESTRING ((1 2), (3 4)) is then accepted (not standard WKT), and malformed strings main accepts are refused, such as LINESTRING ((1 2, 3 4) and a polygon hole without its own ( (expected ( but found: 2). In the study's 200,000-input probe, accepted inputs with unbalanced parentheses went from 1,277-1,338 per run to 0.
  • Stricter is arguably better, but it is a behaviour change outside MULTIPOINT for maintainers to decide on.

What the checks validated

Gradle, same worktree: the repro passes with A, with assertions on and off; the same 18 classes pass (200 tests, 0 failures, 7 skipped). Earlier, with the sketch shadowed: the hunt tests pass and the differential probe finds no well-formed input rejected or misread.

Downstream note

rdf4j's Lucene index (read, not run) catches this exception and logs a warning, so a standard-form multipoint is left out of its geo index and geo queries miss that record.

How it was found

Non-formal: a differential parsing probe fed every WKT geometry type (empty, envelope, 3D, collections) to the parser and compared each result with the shape as written. Two parenthesized points failed; tracing the token reads in parseCoordinates gave the cause, and a public-API JUnit test reproduced it.

The method in full122 words

Method: non-formal (a differential probe, then executed tests). On 2026-10-26 the hunt unit for the LatLon geo queries used, as its secondary method, a probe that parses every WKT geometry type the class supports, including EMPTY, ENVELOPE/BBOX, 3D coordinates and collections, and compares the result with the shape as written. MULTIPOINT ((1 2), (3 4)) was rejected while MULTIPOINT ((1 2)) parsed; following parseCoordinates token by token gave the root cause. A 4-test public-API JUnit (2 controls, 2 bug cases) reproduced it on main (it was later run on 11 releases from 7.6.0 to 10.5.2, 9 by the history step and 2 by the finder); independent checkers then reproduced it with their own tests, and it was cut to the one-test repro.

Prior reports, history and checks

Prior reports

No prior report found in GitHub issues, PRs and commits (incl. the JIRA archive), open PRs, the Lucene mailing lists, downstream repositories or the web (101 queries, 2026-10-26; refreshed 2026-11-01). Two open Elasticsearch issues ask the same of Elasticsearch's own, different parser.

Searches5 searches

Closest items, none a report of this bug: #9584 (LUCENE-8538, the introducing issue; lists MULTIPOINT -> double[][] with no syntax limit); Elasticsearch #138162 (open, 2025-11-17) and #151876 (open, 2026-06-22), the same input refused by Elasticsearch's own WellKnownText parser, different code with a different defect (no parenthesized-point branch at all) whose fix would not touch Lucene (the two issues disagree on which form is "OGC"); Elasticsearch PR #27417 (2017), the code this parser was ported from, which lists both forms as supported; PostGIS PR #682 (2022, a writer that switched to this form). Limits: GitHub search reads only the first 50-100 results per query; the open-PR file snapshot was partial for one very large PR; private reports are not visible.

What was searched, when, and what turned up
WhereWhenHowFound
apache/lucene issues and PRs (incl. the migrated JIRA issues)2026-10-2630 scripted prior-report queries plus 9 manual ones: class, method and symptom names, WKT, MULTIPOINT, parentheses, the error text, LUCENE-8538; all 33 WKT and 4 MULTIPOINT hits readno report; #9584 is the introducing issue
Commits and open PRs2026-10-26GitHub commit search, git log -S isOpenParen, git log -L parseCoordinates; changed files of all 406 open PRslogic unchanged since 2018; no open PR touches the file
Mailing lists and JIRA archive2026-10-26dev@, java-user@, issues@, commits@ (31 queries, with control queries that do hit); 4 JIRA queriesonly the 2018 commit, clean-up commits and unrelated spatial threads
Elasticsearch, OpenSearch, Solr; the web2026-10-2614 repository queries; 7 web queriesElasticsearch #138162 and #151876 (their own parser); nothing in OpenSearch or Solr
apache/lucene issues and PRs (refresh)2026-11-01SimpleWKTShapeParser; MULTIPOINT WKT; "expected ) but found"; WKT created since 2026-10-25nothing new (#9584, #1017, #9595, #5709 and six unrelated hits)

History

Born with the class in LUCENE-8538 (bd714ca1edc, 2018-10-24; backport 5e7858041b8 shipped in 7.6.0). Every release from 7.6.0 to 10.6.0 and main fails; 7.5.0 has no such class. The file is unchanged on upstream main as of 2026-10-30.

Versions tested6 rows

Introduced by LUCENE-8538 "Add Simple WKT Shape Parser" (#9584, by Nick Knize, @nknize; a JIRA patch, no pull request): main-line commit bd714ca1edc (committed 2018-10-24 in lucene/sandbox, first released in 8.0.0) and the same-day branch_7x backport 5e7858041b8 (byte-identical parser file, released in 7.6.0). git blame -w -M -C -C gives bd714ca1edc for every branch and check of parseCoordinates; its signature and two throw statements blame to the LUCENE-9570 reformat 2d6ad2fee6d, which only re-wrapped them, and later commits only clean up. LUCENE-8621 moved the class from lucene-sandbox to lucene-core for 8.5.0 without logic changes. The newest commit touching the file on upstream main, 448c7d8e241 (2026-02-11), predates 645d4fe14a20.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal repro fails with assertions on and off (standalone and through Gradle); passes with fix A or B
10.6.0 affectedminimal repro fails with assertions on and off (release jars)
main ea57360912ed affectedthe hunt's 4-test JUnit fails 2 of 4 with assertions on and off; both controls pass
8.5.0, 8.11.2, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 affectedthe 4-test JUnit fails 2 of 4 with assertions on (10.5.2 also off; class in lucene-core)
7.6.0, 7.7.3, 8.0.0, 8.4.1 first affected 7.6.0the 4-test JUnit fails 2 of 4 with assertions on (class in lucene-sandbox)
7.5.0 no such classthe test does not compile: SimpleWKTShapeParser does not exist

Independent checks

Confirmed, high confidence: the finder, an independent reproducer, an intended-behaviour devil's advocate and an adjudication all reproduced it on main with assertions on and off. A reviewer from another model family, unable to run code, agreed with the root cause.

Checks and verdicts272 words

Everyone agrees on the root cause and that it is a loud wrong error on valid input, never a wrong shape.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidate4-test JUnit fails 2 of 4 on main (assertions on and off; also run on 11 releases with assertions on, 9 by the history step, 7.7.3 and 8.11.2 by the finder); controls pass; 4/4 with the fix sketch, which also passes upstream TestSimpleWKTShapeParsing (14) and TestTessellator (70).
cross-model review, refute lens (OpenAI GPT-5.6, read-only) not real, low (procedural)Could not run code, so its verdict is procedural; its static trace matched the root cause, it rated the intended argument weak and found the TestGeo3d precedent. Separate re-run: 2 of 4 fail.
independent reproducer realIts own JUnit fails 2 of 4 on main (assertions on and off), 10.5.2, 9.12.3 and 8.11.2; 4/4 with the fix; its probe matches a static trace and found the END-OF-STREAM variant.
cross-model review, intended lens (OpenAI GPT-5.6, read-only) real, mediumThe works-as-designed case is weak.
intended-behaviour devil's advocate real, low severityBuilt the strongest intended case and rejected it (position-dependent acceptance; the Elasticsearch original checks the first ) before its loop); main and 10.5.2 give identical output.
adjudication confirmed, highRe-ran the reproducer's JUnit with a fresh seed (2 of 4 fail, assertions on and off) and the finder's.
Registration check, audit and this doc confirmedFresh-seed re-runs fail on main and 10.5.2 and pass with the fix; the one-test repro fails on 645d4fe14a20 and 10.6.0 and through gradle; fixes A and B validated through gradle.

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

Finding record

LFV-570, snapshot of 2026-11-06

Registry title
SimpleWKTShapeParser rejects a MULTIPOINT whose points each sit in their own parentheses, e.g. MULTIPOINT ((1 2), (3 4)), with ParseException "expected ) but found: ','", although it accepts MULTIPOINT ((1 2)) and MULTIPOINT (1 2, 3 4) (every release since 7.6.0)
Area
core / geo WKT parsing (SimpleWKTShapeParser)
Priority
Low, rank 1070: The public core SimpleWKTShapeParser loudly rejects the standard parenthesized MULTIPOINT form with an easy workaround and known downstream users.
Impact class
wrong error
Impact note
Hits applications that call SimpleWKTShapeParser.parse or parseExpectedType (a public class in the exported core package org.apache.lucene.geo with no @lucene.internal or @lucene.experimental tag; only tests call it in the Lucene tree, and a read-only code search found downstream users such as rdf4j's Lucene index and its copy in INCEpTION, plus Anserini, whose generator does not handle MULTIPOINT) with MULTIPOINT text in the standard form, where each point has its own parentheses: the geometry, or the GEOMETRYCOLLECTION that holds it, is rejected with a checked ParseException; no setting is involved, no wrong shape is ever returned, nothing hangs, no index is touched and no scale is needed, with -ea and -da, in every release from 7.6.0 to 10.6.0 (10.6.0 executed with the minimal repro) and on main; the bare form MULTIPOINT (1 2, 3 4) still parses, so callers who control the text have a workaround.
Summary
You call SimpleWKTShapeParser.parse("MULTIPOINT ((1 2), (3 4))"), the Simple Features WKT form in which each point of a multipoint sits in its own parentheses, and expect double[][] {{1, 2}, {3, 4}}, because the class says it parses WKT in compliance with ISO/IEC 13249-3:2016 and it already accepts MULTIPOINT ((1 2)) and the bare legacy form MULTIPOINT (1 2, 3 4). Instead it throws java.text.ParseException "expected ) but found: ','", also inside a GEOMETRYCOLLECTION and with 3D points. The cause is in parseCoordinates: when the first point starts with '(' the code before the loop does not read that point's ')', so the loop takes that ')' as the end of the whole list and the check after the loop then finds the ',' where it wants the missing ')'; whether a parenthesized point is accepted therefore depends on where it sits (MULTIPOINT (1 2, (3 4), 5 6) parses, while MULTIPOINT (1 2, (3 4)) fails with 'expected word but found: END-OF-STREAM').
Introduced
bd714ca1edc681cd871e6d385bd98a9cb9d7ffe6 (main line, LUCENE-8538 'Add Simple WKT Shape Parser' by @nknize, migrated as apache/lucene#9584; JIRA patch era, no pull request; authored 2018-10-22, committed 2018-10-24 to master in lucene/sandbox; first shipped in 8.0.0) and its branch_7x backport 5e7858041b87fc9091b0aae7c228445ab6566231 (same day, byte-identical parser file; shipped in 7.6.0). The faulty order was born with the class and is not a regression [read: git blame -w -M -C -C on main lines 121-145 gives bd714ca1edc for every logic line, and 2d6ad2fee6d (LUCENE-9570 reformat) only re-wrapped the two throws; a whitespace-insensitive diff from bd714ca1edc to main touches nothing in parseCoordinates, parseMultiPoint, nextCloser, nextCloserOrComma, nextWord or isNumberNext; exec: 7.5.0 has no class (compile error), 7.6.0 fails] (2018-10-24)
First affected version
7.6.0 [exec] (branch_7x backport; the class lived in lucene-sandbox from 7.6.0 to 8.4.1 and in lucene-core from 8.5.0, moved by LUCENE-8621 without logic changes); first main-line release 8.0.0 [exec]; 7.5.0 has no SimpleWKTShapeParser
Affected versions
every release from 7.6.0 through 10.6.0, and main ea57360912ed; the one-test minimal repro fails on the 10.6.0 release jars (SHA-1 checked against Maven Central) with -ea and -da [exec 2026-11-01]. [exec] the finder's JUnit (2 controls, 2 bug tests) fails 2 of 4 with both controls passing on 7.6.0 (junitcore runner, because 7.6.0 pins JUnit 4.10), 7.7.3, 8.0.0, 8.4.1 (last sandbox release), 8.5.0 (first in core), 8.11.2, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da); the independent reproduction's JUnit fails 2 of 4 on main (-ea and -da), 10.5.2, 9.12.3 and 8.11.2 (sandbox jar); re-runs on main -ea and -da and 10.5.2 -da. [read] the logic is unchanged since the introducing commit, so the releases not executed are affected by inference; branch_10_6 (released as 10.6.0 on 2026-10-29) and the unreleased branch_10x carry it. Not fixed upstream: the file has no diff between ea57360912ed and upstream main a9779ef3c19 (2026-10-26). aarch64 with JDK 25 only (pure string parsing)
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
non-formal
Upstream and prior reports
none found (2026-10-26, read-only): a scripted prior-report search (30 GitHub issue/PR queries on apache/lucene, 3 commit, 4 downstream-repository, 20 mailing-list over dev@, java-user@, issues@ and commits@, 4 JIRA-archive, and web) plus 34 manual gh, list, git and web queries; the finder's run (7 queries) and the intended-behaviour check's (20) agree. Nothing reports or discusses SimpleWKTShapeParser rejecting MULTIPOINT points in parentheses, and no open PR touches the file. Closest items, not reports of this bug: #9584 (LUCENE-8538), the introducing issue, which lists 'MULTIPOINT -> double[][]' with no syntax limit; Elasticsearch #138162 (open, 2025-11-17, >bug) and #151876 (open, 2026-06-22, >tech-debt), the same standard MULTIPOINT form rejected by Elasticsearch's own WellKnownText parser, different code with a different defect (no parenthesized-point branch at all) whose fix would not touch Lucene; Elasticsearch PR #27417 (merged 2017-12-05), the GeoWKTParser this class was ported from, whose description lists both MULTIPOINT forms as supported. Search limits: GitHub search reads only the first 50-100 results per query; the open-PR file snapshot is partial for one very large PR; one Q&A page was seen as a snippet only; private channels were not searched
Lucene commit tested
main 645d4fe14a20 (the minimal repro with assertions on and off, standalone and through gradle; fixes A and B in a scratch worktree); the 10.6.0 release jars (assertions on and off); the hunt's 4-test JUnit, with assertions on, on 7.6.0, 7.7.3, 8.0.0, 8.4.1, 8.5.0, 8.11.2, 8.11.4, 9.0.0, 9.12.3, 10.0.0 and 10.5.2 (10.5.2 also with assertions off) and, with assertions on and off, on main ea57360912ed, whose parser file is identical to 645d4fe14a20's; 7.5.0 has no such class. Upstream main 2f424fd428da (2026-10-30) has the same file (read).

Lucene and upstream sources

Finding record
  • lucene/core/src/java/org/apache/lucene/geo/SimpleWKTShapeParser.java:29-34 (at ea57360912ed) (class javadoc: 'Parses shape geometry represented in WKT format', 'complies with OGC document: 12-063r5 and ISO/IEC 13249-3:2016'), :35 (public class, no internal/experimental tag), :50-55 (public static parse and parseExpectedType, checked ParseException), :122-145 (parseCoordinates: first coordinate at :125-128 without its ')', loop condition nextCloserOrComma at :130, in-loop '(' branch with its own ')' check at :131-139, check after the loop at :141-144), :159-173 (parseMultiPoint delegates to parseCoordinates), :203-210 (parsePolygonHole relies on the first-position '(' branch), :347-353 (nextCloser throws "expected ) but found"), :374 (nextCloserOrComma)
  • lucene/core/src/java/module-info.java:38 (at ea57360912ed) (exports org.apache.lucene.geo); lucene/core/src/test/org/apache/lucene/geo/TestSimpleWKTShapeParsing.java:45-48 (testMultiPoint uses only the bare form); lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/TestGeo3d.java:42 (the parenthesized form given to a different WKT reader); grep of lucene/*/src/java: no non-test caller
  • SECURITY.md lines 28, 38, 42, 46, 50 (threat model)
  • a Lucene clone origin/main a9779ef3c19 (2026-10-26): git diff ea57360912ed origin/main on SimpleWKTShapeParser.java is empty
The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/geo/SimpleWKTShapeParser.java:50-55, 78-90, 122-145, 148-156, 159-173, 203-210, 282-295, 347-353, 374-383 (at 645d4fe14a20)
Who is affected
  • lucene/core/src/java/org/apache/lucene/geo/SimpleWKTShapeParser.java:35, 50-55 (at 645d4fe14a20); lucene/core/src/java/module-info.java:38
  • grep of lucene/*/src at 645d4fe14a20: SimpleWKTShapeParser is used only by TestSimpleWKTShapeParsing, TestTessellator and TestShapeDocValues
  • github.com/postgis/postgis/pull/682 (title, merged 2022-04-12; read-only GitHub API, 2026-11-01)
  • github.com/elastic/elasticsearch/issues/151876 and /issues/138162 (state, dates, body; read-only GitHub API, 2026-11-01)
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/geo/SimpleWKTShapeParser.java:29-34, 126, 131-138, 141-144, 203-210 (at 645d4fe14a20)
  • lucene/core/src/test/org/apache/lucene/geo/TestSimpleWKTShapeParsing.java:45-60 (at 645d4fe14a20)
  • lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/TestGeo3d.java:42 (at 645d4fe14a20)
  • lucene/CHANGES.txt:7093-7094 (7.6.0 section, LUCENE-8538)
Prior reports
  • read-only GitHub search API, 2026-11-01 (4 queries on apache/lucene)
History
  • read-only GitHub API, 2026-11-01: commits touching the file on apache/lucene main (newest 448c7d8e241); main at 2f424fd428da

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 from the code at 645d4fe14a20 and the repro's input and output.

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.