[Git][debian-gis-team/mapbox-polylabel][master] 7 commits: New upstream version 2.1.0

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Fri Jul 3 12:03:25 BST 2026



Bas Couwenberg pushed to branch master at Debian GIS Project / mapbox-polylabel


Commits:
6b5982af by Bas Couwenberg at 2026-06-25T13:58:58+02:00
New upstream version 2.1.0
- - - - -
665cf175 by Bas Couwenberg at 2026-07-03T11:51:18+02:00
Update upstream to Mapnik fork, Mapbox dropped C++ implementation.

- - - - -
e5eac8d1 by Bas Couwenberg at 2026-07-03T12:55:01+02:00
New upstream version 2.1.0
- - - - -
acf8e6d3 by Bas Couwenberg at 2026-07-03T12:55:02+02:00
Update upstream source from tag 'upstream/2.1.0'

Update to upstream version '2.1.0'
with Debian dir 037484943ab5e23eee276d03178bc7d0be7acf95
- - - - -
e01886d7 by Bas Couwenberg at 2026-07-03T12:55:19+02:00
New upstream release.

- - - - -
b4d011e8 by Bas Couwenberg at 2026-07-03T12:56:48+02:00
Update copyright file.

- - - - -
ea7d8ac5 by Bas Couwenberg at 2026-07-03T12:58:27+02:00
Set distribution to unstable.

- - - - -


22 changed files:

- + .github/workflows/npm-release.yml
- .github/workflows/test.yml
- .gitignore
- − .gitmodules
- + CODEOWNERS
- LICENSE
- − Makefile
- README.md
- + bench.js
- + bench/bench.cpp
- debian/changelog
- debian/control
- debian/copyright
- debian/upstream/metadata
- debian/watch
- − debug.js
- include/mapbox/polylabel.hpp
- package-lock.json
- package.json
- polylabel.js
- test/test.cpp
- test/test.js


Changes:

=====================================
.github/workflows/npm-release.yml
=====================================
@@ -0,0 +1,17 @@
+name: Publish
+on:
+  push:
+    tags: ['v*']
+permissions:
+  id-token: write
+jobs:
+  publish:
+    runs-on: ubuntu-latest
+    environment: npm-release
+    steps:
+      - uses: actions/checkout at v6
+      - uses: actions/setup-node at v6
+        with:
+          node-version: 24
+      - run: npm ci
+      - run: npm publish --access public


=====================================
.github/workflows/test.yml
=====================================
@@ -4,24 +4,10 @@ jobs:
   node:
     runs-on: ubuntu-latest
     steps:
-    - name: Checkout
-      uses: actions/checkout at v2
-
-    - name: Setup Node
-      uses: actions/setup-node at v1
+    - uses: actions/checkout at v6
+    - uses: actions/setup-node at v6
       with:
-        node-version: 20
-
-    - name: Install dependencies
-      run: npm ci
-
-    - name: Run tests
-      run: npm test
-  cpp:
-      runs-on: ubuntu-latest
-      steps:
-      - name: Checkout
-        uses: actions/checkout at v2
-
-      - name: Test C++
-        run: make test
+        node-version: 24
+        cache: npm
+    - run: npm ci
+    - run: npm test


=====================================
.gitignore
=====================================
@@ -1,3 +1 @@
-mason_packages
 node_modules
-build


=====================================
.gitmodules deleted
=====================================
@@ -1,3 +0,0 @@
-[submodule ".mason"]
-	path = .mason
-	url = https://github.com/mapbox/mason


=====================================
CODEOWNERS
=====================================
@@ -0,0 +1 @@
+*   @mapbox/gl-js


=====================================
LICENSE
=====================================
@@ -1,5 +1,5 @@
 ISC License
-Copyright (c) 2016 Mapbox
+Copyright (c) 2026 Mapbox
 
 Permission to use, copy, modify, and/or distribute this software for any purpose
 with or without fee is hereby granted, provided that the above copyright notice


=====================================
Makefile deleted
=====================================
@@ -1,27 +0,0 @@
-CXXFLAGS += -Iinclude -std=c++14 -Wall -Wextra -Wshadow -Werror -Wno-class-memaccess -g -fPIC
-
-MASON ?= .mason/mason
-VARIANT = variant 1.1.4
-GEOMETRY = geometry 0.9.2
-RAPIDJSON = rapidjson 1.1.0
-
-$(MASON):
-	git submodule update --init
-
-DEPS = `$(MASON) cflags $(VARIANT)` \
-	`$(MASON) cflags $(GEOMETRY)` \
-	`$(MASON) cflags $(RAPIDJSON)`
-
-mason_packages/headers/geometry: $(MASON)
-	$(MASON) install $(VARIANT)
-	$(MASON) install $(GEOMETRY)
-	$(MASON) install $(RAPIDJSON)
-
-build:
-	mkdir -p build
-
-build/test: test/test.cpp include/mapbox/polylabel.hpp build mason_packages/headers/geometry
-	$(CXX) $(CFLAGS) $(CXXFLAGS) $(DEPS) $< -o $@
-
-test: build/test
-	./build/test


=====================================
README.md
=====================================
@@ -48,20 +48,6 @@ Be careful to pick precision appropriate for the input units. E.g. in case of ge
 [TypeScript type definitions](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/polylabel)
 are available via `npm install --save @types/polylabel`.
 
-### C++ Usage
-
-It is recommended to install polylabel via [mason](https://github.com/mapbox/mason). You will also need to install its dependencies: [geometry.hpp](https://github.com/mapbox/geometry.hpp) and [variant](https://github.com/mapbox/variant).
-
-```C++
-#include <mapbox/polylabel.hpp>
-
-int main() {
-    mapbox::geometry::polygon<double> polygon = readPolygon(); // Get polygon data from somewhere.
-    mapbox::geometry::point<double> p = mapbox::polylabel(polygon, 1.0);
-    return 0;
-}
-```
-
 #### Ports to other languages
 
 - [andrewharvey/geojson-polygon-labels](https://github.com/andrewharvey/geojson-polygon-labels) (CLI) 
@@ -69,8 +55,10 @@ int main() {
 - [Shapely](https://github.com/Toblerity/Shapely/blob/master/shapely/algorithms/polylabel.py) (Python)
 - [polylabelr](https://CRAN.R-project.org/package=polylabelr) (R)
 - [polylabel-rs](https://github.com/urschrei/polylabel-rs) (Rust)
-- [polylabel-java](https://github.com/FreshLlamanade/polylabel-java) (Java)
 - [php-polylabel](https://github.com/dliebner/php-polylabel) (PHP)
 - [dart_polylabel](https://github.com/beroso/dart_polylabel) (Dart)
 - [ruby-polylabel](https://github.com/fredplante/ruby-polylabel) (Ruby)
 - [Polylabel.jl](https://github.com/asinghvi17/Polylabel.jl) (Julia)
+- [geobase](https://github.com/navibyte/geospatial/blob/main/dart/geobase/lib/src/geometric/cartesian/areal/polylabel.dart) (Dart)
+- [PolylabelNet](https://github.com/oberbichler/PolylabelNet) (C#)
+- [polylabel-geometrynodes](https://github.com/farukahmet/polylabel-geometrynodes) (Blender)


=====================================
bench.js
=====================================
@@ -0,0 +1,24 @@
+import polylabel from './polylabel.js';
+import water1 from './test/fixtures/water1.json' with {type: 'json'};
+import water2 from './test/fixtures/water2.json' with {type: 'json'};
+
+const cases = [
+    ['water1 precision  1', water1, 1],
+    ['water1 precision 50', water1, 50],
+    ['water2 precision  1', water2, 1],
+];
+
+const RUNS = 100;
+
+for (const [name, polygon, precision] of cases) {
+    const times = [];
+    for (let r = 0; r < RUNS; r++) {
+        const t = performance.now();
+        polylabel(polygon, precision);
+        times.push(performance.now() - t);
+    }
+    times.sort((a, b) => a - b);
+    const min = times[0];
+    const median = times[times.length >> 1];
+    console.log(`${name}: min ${min.toFixed(2)}ms, median ${median.toFixed(2)}ms`);
+}


=====================================
bench/bench.cpp
=====================================
@@ -0,0 +1,172 @@
+#include <mapbox/polylabel.hpp>
+
+#include <chrono>
+#include <cctype>
+#include <cmath>
+#include <cstdlib>
+#include <fstream>
+#include <iomanip>
+#include <iostream>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace mapbox;
+
+volatile double sink = 0;
+
+class JsonFixtureParser {
+public:
+    explicit JsonFixtureParser(std::string input_) : input(std::move(input_)) {}
+
+    geometry::polygon<double> parsePolygon() {
+        geometry::polygon<double> polygon;
+        consume('[');
+        skipWhitespace();
+        while (!tryConsume(']')) {
+            polygon.push_back(parseRing());
+            skipWhitespace();
+            tryConsume(',');
+        }
+        return polygon;
+    }
+
+private:
+    geometry::linear_ring<double> parseRing() {
+        geometry::linear_ring<double> ring;
+        consume('[');
+        skipWhitespace();
+        while (!tryConsume(']')) {
+            consume('[');
+            const double x = parseNumber();
+            consume(',');
+            const double y = parseNumber();
+            consume(']');
+            ring.push_back({x, y});
+            skipWhitespace();
+            tryConsume(',');
+        }
+        return ring;
+    }
+
+    double parseNumber() {
+        skipWhitespace();
+        const char* begin = input.c_str() + pos;
+        char* end = nullptr;
+        const double value = std::strtod(begin, &end);
+        if (end == begin) throw std::runtime_error("expected number");
+        pos += static_cast<std::size_t>(end - begin);
+        skipWhitespace();
+        return value;
+    }
+
+    void consume(char expected) {
+        skipWhitespace();
+        if (pos >= input.size() || input[pos] != expected) {
+            throw std::runtime_error(std::string("expected '") + expected + "'");
+        }
+        ++pos;
+        skipWhitespace();
+    }
+
+    bool tryConsume(char expected) {
+        skipWhitespace();
+        if (pos < input.size() && input[pos] == expected) {
+            ++pos;
+            skipWhitespace();
+            return true;
+        }
+        return false;
+    }
+
+    void skipWhitespace() {
+        while (pos < input.size() && std::isspace(static_cast<unsigned char>(input[pos]))) {
+            ++pos;
+        }
+    }
+
+    std::string input;
+    std::size_t pos = 0;
+};
+
+geometry::polygon<double> fixture(const std::string& path) {
+    std::ifstream file(path.c_str());
+    std::stringstream buffer;
+    buffer << file.rdbuf();
+    return JsonFixtureParser(buffer.str()).parsePolygon();
+}
+
+geometry::linear_ring<double> regularRing(std::size_t n, double cx, double cy, double radius, bool clockwise) {
+    constexpr double pi = 3.141592653589793238462643383279502884;
+    geometry::linear_ring<double> ring;
+    ring.reserve(n + 1);
+
+    for (std::size_t i = 0; i < n; ++i) {
+        const std::size_t index = clockwise ? n - i : i;
+        const double angle = 2.0 * pi * static_cast<double>(index) / static_cast<double>(n);
+        ring.push_back({cx + std::cos(angle) * radius, cy + std::sin(angle) * radius});
+    }
+    ring.push_back(ring.front());
+    return ring;
+}
+
+geometry::polygon<double> donut(std::size_t n) {
+    geometry::polygon<double> polygon;
+    polygon.push_back(regularRing(n, 0, 0, 5000, false));
+    polygon.push_back(regularRing(n, 0, 0, 2200, true));
+    return polygon;
+}
+
+geometry::polygon<double> sawtoothSquare(std::size_t teethPerSide) {
+    geometry::linear_ring<double> ring;
+    ring.reserve(teethPerSide * 4 + 1);
+    const double side = 10000;
+    const double step = side / static_cast<double>(teethPerSide);
+
+    for (std::size_t i = 0; i < teethPerSide; ++i) ring.push_back({-5000 + i * step, -5000 - (i % 2 ? 12.0 : 0.0)});
+    for (std::size_t i = 0; i < teethPerSide; ++i) ring.push_back({5000 + (i % 2 ? 12.0 : 0.0), -5000 + i * step});
+    for (std::size_t i = 0; i < teethPerSide; ++i) ring.push_back({5000 - i * step, 5000 + (i % 2 ? 12.0 : 0.0)});
+    for (std::size_t i = 0; i < teethPerSide; ++i) ring.push_back({-5000 - (i % 2 ? 12.0 : 0.0), 5000 - i * step});
+    ring.push_back(ring.front());
+
+    return geometry::polygon<double>({ring});
+}
+
+void runCase(const std::string& name, const geometry::polygon<double>& polygon, double precision, std::size_t iterations) {
+    auto p = polylabel(polygon, precision);
+    sink += p.x + p.y;
+
+    const auto start = std::chrono::steady_clock::now();
+    double checksum = 0;
+    for (std::size_t i = 0; i < iterations; ++i) {
+        p = polylabel(polygon, precision);
+        checksum += p.x * 0.0000001 + p.y * 0.0000003;
+    }
+    const auto stop = std::chrono::steady_clock::now();
+    const std::chrono::duration<double, std::milli> elapsed = stop - start;
+    sink += checksum;
+
+    std::cout << std::left << std::setw(26) << name
+              << " iterations=" << std::setw(5) << iterations
+              << " total_ms=" << std::setw(12) << std::fixed << std::setprecision(3) << elapsed.count()
+              << " ms_per_call=" << std::setw(10) << std::setprecision(6) << elapsed.count() / iterations
+              << " result=(" << std::setprecision(6) << p.x << ", " << p.y << ")"
+              << '\n';
+}
+
+int main() {
+    const auto water1 = fixture("./test/fixtures/water1.json");
+    const auto water2 = fixture("./test/fixtures/water2.json");
+    const auto donut4096 = donut(4096);
+    const auto square8192 = sawtoothSquare(2048);
+
+    runCase("water1 precision 1", water1, 1.0, 400);
+    runCase("water1 precision 50", water1, 50.0, 2000);
+    runCase("water2 precision 1", water2, 1.0, 1000);
+    runCase("donut 8194 vertices", donut4096, 5.0, 80);
+    runCase("square 8193 vertices", square8192, 5.0, 80);
+
+    std::cerr << "sink=" << sink << '\n';
+}


=====================================
debian/changelog
=====================================
@@ -1,13 +1,15 @@
-mapbox-polylabel (2.0.1-2) UNRELEASED; urgency=medium
+mapbox-polylabel (2.1.0-1) unstable; urgency=medium
 
+  * New upstream release.
   * Bump Standards-Version to 4.7.4, no changes.
   * Update lintian overrides.
   * Drop Rules-Requires-Root: no, default since dpkg 1.22.13.
   * Use test-build-validate-cleanup instead of test-build-twice.
   * Drop Priority: optional, default since dpkg 1.22.13.
-  * Limit watch file to 2.0.x releases, 2.1 dropped the C++ implementation.
+  * Update upstream to Mapnik fork, Mapbox dropped C++ implementation.
+  * Update copyright file.
 
- -- Bas Couwenberg <sebastic at debian.org>  Sun, 28 Jul 2024 19:43:49 +0200
+ -- Bas Couwenberg <sebastic at debian.org>  Fri, 03 Jul 2026 12:58:12 +0200
 
 mapbox-polylabel (2.0.1-1) unstable; urgency=medium
 


=====================================
debian/control
=====================================
@@ -8,7 +8,7 @@ Build-Depends: debhelper-compat (= 13),
 Standards-Version: 4.7.4
 Vcs-Browser: https://salsa.debian.org/debian-gis-team/mapbox-polylabel/
 Vcs-Git: https://salsa.debian.org/debian-gis-team/mapbox-polylabel.git
-Homepage: https://github.com/mapbox/polylabel
+Homepage: https://github.com/mapnik/polylabel
 
 Package: libmapbox-polylabel-dev
 Architecture: all


=====================================
debian/copyright
=====================================
@@ -1,10 +1,10 @@
 Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
 Upstream-Name: polylabel
-Upstream-Contact: Mapbox (https://github.com/mapbox/polylabel/issues)
-Source: https://github.com/mapbox/polylabel
+Upstream-Contact: Mapnik developers (https://github.com/mapnik/polylabel/issues)
+Source: https://github.com/mapnik/polylabel
 
 Files: *
-Copyright: 2016, Mapbox
+Copyright: 2026, Mapbox
 License: ISC
 
 Files: debian/*


=====================================
debian/upstream/metadata
=====================================
@@ -1,5 +1,5 @@
 ---
-Bug-Database: https://github.com/mapbox/polylabel/issues
-Bug-Submit: https://github.com/mapbox/polylabel/issues/new
-Repository: https://github.com/mapbox/polylabel.git
-Repository-Browse: https://github.com/mapbox/polylabel
+Bug-Database: https://github.com/mapnik/polylabel/issues
+Bug-Submit: https://github.com/mapnik/polylabel/issues/new
+Repository: https://github.com/mapnik/polylabel.git
+Repository-Browse: https://github.com/mapnik/polylabel


=====================================
debian/watch
=====================================
@@ -3,5 +3,5 @@ opts=\
 dversionmangle=s/\+(debian|dfsg|ds|deb)\d*$//,\
 uversionmangle=s/(\d)[_\.\-\+]?((RC|rc|pre|dev|beta|alpha)\d*)$/$1~$2/,\
 filenamemangle=s/(?:.*\/)?(?:rel|v|polylabel)[\-\_]?(\d\S+)\.(tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))/polylabel-$1.$2/ \
-https://github.com/mapbox/polylabel/tags \
-(?:.*?/archive/(?:.*?/)?)?(?:rel|v|polylabel)[\-\_]?(2\.0\.\S+)\.(?:tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
+https://github.com/mapnik/polylabel/tags \
+(?:.*?/archive/(?:.*?/)?)?(?:rel|v|polylabel)[\-\_]?(\d\S+)\.(?:tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))


=====================================
debug.js deleted
=====================================
@@ -1,13 +0,0 @@
-
-import polylabel from './polylabel.js';
-import {readFileSync} from 'fs';
-
-const polygon = JSON.parse(readFileSync(new URL('./test/fixtures/water1.json', import.meta.url)));
-
-console.log(`num points: ${[].concat(...polygon).length}`);
-
-console.time('find point');
-const result = polylabel(polygon, 1, true);
-console.timeEnd('find point');
-
-console.log(result);


=====================================
include/mapbox/polylabel.hpp
=====================================
@@ -1,5 +1,37 @@
 #pragma once
 
+// polylabel — find the pole of inaccessibility (most distant internal point)
+// of a polygon, useful for optimal label placement.
+//
+// This is the C++ port of polylabel v2.1.0 (https://github.com/mapbox/polylabel),
+// preserving the public API and mapbox/geometry.hpp dependencies of the prior
+// v2.0.1 C++ implementation while bringing in all v2.1.0 algorithmic
+// improvements:
+//
+//   * Clamped initial cell size: cellSize = max(precision, min(width, height)),
+//     with an immediate early-out when min(width, height) <= precision (covers
+//     degenerate / sub-precision polygons).
+//   * Flattened, cache-friendly coordinate buffer + per-ring end offsets,
+//     eliminating pointer-chasing in the hot distance loop.
+//   * Block-based bounding-box skipping: K consecutive edges share one bbox so
+//     whole blocks are skipped in O(1) when they can neither improve the
+//     distance nor flip the ray-cast parity.
+//   * Early-out distance threshold: a child cell whose distance is provably
+//     unable to beat the current best (or to be worth subdividing) bails out
+//     of the outline scan as soon as that fact is established.
+//   * Seed-based segment warm-start: each cell records its nearest segment and
+//     hands it to its children, which almost always share the same nearest
+//     segment and thus reach the early-out threshold without a full scan.
+//   * Aggressive queue pruning: a cell is enqueued only if it can still beat
+//     the best by more than `precision`; the main loop can `break` (not just
+//     `continue`) on the first cell that cannot, because the priority queue is
+//     ordered by decreasing potential.
+//   * Centroid fallback: if the outer ring has zero area or the centroid lands
+//     outside the polygon, fall back to the first vertex.
+//
+// The result is bit-identical to v2.0.1 for all existing test fixtures while
+// being substantially faster on large polygons.
+
 #include <mapbox/geometry/polygon.hpp>
 #include <mapbox/geometry/envelope.hpp>
 #include <mapbox/geometry/point.hpp>
@@ -7,164 +39,351 @@
 
 #include <algorithm>
 #include <cmath>
+#include <cstddef>
 #include <iostream>
+#include <limits>
 #include <queue>
+#include <vector>
 
 namespace mapbox {
 
 namespace detail {
 
-// get squared distance from a point to a segment
+// number of consecutive edges grouped under a single bounding box for block-skip
+constexpr std::size_t K = 32;
+
+// get squared distance from a point (px, py) to a segment (x, y)-(bx, by)
 template <class T>
-T getSegDistSq(const geometry::point<T>& p,
-               const geometry::point<T>& a,
-               const geometry::point<T>& b) {
-    auto x = a.x;
-    auto y = a.y;
-    auto dx = b.x - x;
-    auto dy = b.y - y;
+T getSegDistSq(T px, T py, T x, T y, T bx, T by) {
+    auto dx = bx - x;
+    auto dy = by - y;
 
     if (dx != 0 || dy != 0) {
-
-        auto t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);
+        auto t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);
 
         if (t > 1) {
-            x = b.x;
-            y = b.y;
-
+            x = bx;
+            y = by;
         } else if (t > 0) {
             x += dx * t;
             y += dy * t;
         }
     }
 
-    dx = p.x - x;
-    dy = p.y - y;
+    dx = px - x;
+    dy = py - y;
 
     return dx * dx + dy * dy;
 }
 
-// signed distance from point to polygon outline (negative if point is outside)
+// flattened polygon: a contiguous coordinate buffer + ring end offsets +
+// precomputed per-block bounding boxes for O(1) block-skip in the hot distance
+// loop. Built once per polylabel() call and shared (by const reference) across
+// all cells.
 template <class T>
-auto pointToPolygonDist(const geometry::point<T>& point, const geometry::polygon<T>& polygon) {
-    bool inside = false;
-    auto minDistSq = std::numeric_limits<double>::infinity();
+struct FlatPolygon {
+    std::vector<T> coords;             // x0,y0,x1,y1,... (rings concatenated)
+    std::vector<std::size_t> ringEnds; // end offset (exclusive) into coords, per ring
+    std::vector<T> blocks;             // minX,minY,maxX,maxY per block of K edges
+};
+
+template <class T>
+FlatPolygon<T> flatten(const geometry::polygon<T>& polygon) {
+    FlatPolygon<T> fp;
+
+    std::size_t numPoints = 0;
+    for (const auto& ring : polygon) numPoints += ring.size();
+    fp.coords.reserve(numPoints * 2);
+    fp.ringEnds.reserve(polygon.size());
 
     for (const auto& ring : polygon) {
-        for (std::size_t i = 0, len = ring.size(), j = len - 1; i < len; j = i++) {
-            const auto& a = ring[i];
-            const auto& b = ring[j];
+        for (const auto& p : ring) {
+            fp.coords.push_back(p.x);
+            fp.coords.push_back(p.y);
+        }
+        fp.ringEnds.push_back(fp.coords.size());
+    }
 
-            if ((a.y > point.y) != (b.y > point.y) &&
-                (point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x)) inside = !inside;
+    // precompute one bounding box per block of K consecutive edges (over both
+    // endpoints of every edge in it) so the distance scan can skip whole blocks
+    // in O(1). The block layout mirrors the flattened coords/ringEnds and is
+    // re-derived in the scan, so only the bboxes need storing: a flat
+    // [minX,minY,maxX,maxY] run per block.
+    const std::size_t stride = K * 2;
+    std::size_t numBlocks = 0;
+    std::size_t ringStart = 0;
+    for (std::size_t r = 0; r < fp.ringEnds.size(); r++) {
+        numBlocks += (fp.ringEnds[r] - ringStart + stride - 1) / stride; // ceil
+        ringStart = fp.ringEnds[r];
+    }
 
-            minDistSq = std::min(minDistSq, getSegDistSq(point, a, b));
+    fp.blocks.resize(numBlocks * 4);
+    std::size_t g = 0;
+    ringStart = 0;
+    for (std::size_t r = 0; r < fp.ringEnds.size(); r++) {
+        const std::size_t ringEnd = fp.ringEnds[r];
+        for (std::size_t s = ringStart; s < ringEnd; s += stride, g += 4) {
+            const std::size_t end = (s + stride < ringEnd) ? s + stride : ringEnd;
+            // include the vertex preceding the block so the block's first edge
+            // (which connects to it) is accounted for in the bbox
+            const std::size_t prev = (s == ringStart) ? ringEnd - 2 : s - 2;
+
+            T minX = fp.coords[prev], minY = fp.coords[prev + 1];
+            T maxX = minX, maxY = minY;
+            for (std::size_t i = s; i < end; i += 2) {
+                const T px = fp.coords[i], py = fp.coords[i + 1];
+                if (px < minX) minX = px; else if (px > maxX) maxX = px;
+                if (py < minY) minY = py; else if (py > maxY) maxY = py;
+            }
+            fp.blocks[g] = minX;
+            fp.blocks[g + 1] = minY;
+            fp.blocks[g + 2] = maxX;
+            fp.blocks[g + 3] = maxY;
         }
+        ringStart = ringEnd;
     }
 
-    return (inside ? 1 : -1) * std::sqrt(minDistSq);
+    return fp;
 }
 
+template <class T>
+struct Cell;
+
+// signed distance from cell center to polygon outline (negative if outside),
+// also recording the nearest segment on the cell. maxD is a distance threshold:
+// if a partial result proves the center is no farther than maxD from the
+// outline, the scan bails out early and returns maxD, since the caller has
+// already determined such a cell can't beat the best. seed is the parent cell
+// (or nullptr); its nearest segment is checked first so boundary cells reach
+// the early-out threshold without scanning the whole outline.
+template <class T>
+T pointToPolygonDist(Cell<T>& cell, const FlatPolygon<T>& fp, T maxD, const Cell<T>* seed);
+
 template <class T>
 struct Cell {
-    Cell(const geometry::point<T>& c_, T h_, const geometry::polygon<T>& polygon)
-        : c(c_),
+    Cell(T x_, T y_, T h_, const FlatPolygon<T>& fp, T maxD, const Cell<T>* seed)
+        : x(x_),
+          y(y_),
           h(h_),
-          d(pointToPolygonDist(c, polygon)),
-          max(d + h * std::sqrt(2))
+          nsx1(0), nsy1(0), nsx2(0), nsy2(0),
+          d(pointToPolygonDist(*this, fp, maxD, seed)),
+          max(d + h * std::sqrt(static_cast<T>(2)))
         {}
 
-    geometry::point<T> c; // cell center
-    T h; // half the cell size
-    T d; // distance from cell center to polygon
+    T x;  // cell center x
+    T y;  // cell center y
+    T h;  // half the cell size
+    // nsx1..nsy2 hold the nearest segment found below, so child cells can seed
+    // their scan with it (a child is almost always nearest to the same segment)
+    T nsx1, nsy1, nsx2, nsy2;
+    T d;   // distance from cell center to polygon
     T max; // max distance to polygon within a cell
 };
 
-// get polygon centroid
 template <class T>
-Cell<T> getCentroidCell(const geometry::polygon<T>& polygon) {
+T pointToPolygonDist(Cell<T>& cell, const FlatPolygon<T>& fp, T maxD, const Cell<T>* seed) {
+    const T x = cell.x;
+    const T y = cell.y;
+    bool inside = false;
+    T minDistSq = std::numeric_limits<T>::infinity();
+    const T thresholdSq = maxD > 0 ? maxD * maxD : T(-1);
+
+    if (seed != nullptr) {
+        cell.nsx1 = seed->nsx1;
+        cell.nsy1 = seed->nsy1;
+        cell.nsx2 = seed->nsx2;
+        cell.nsy2 = seed->nsy2;
+        minDistSq = getSegDistSq(x, y, seed->nsx1, seed->nsy1, seed->nsx2, seed->nsy2);
+        if (minDistSq <= thresholdSq) return maxD;
+    }
+
+    const std::size_t stride = K * 2;
+    const std::size_t numRings = fp.ringEnds.size();
+    std::size_t g = 0; // running block index into bboxes
+    std::size_t ringStart = 0;
+
+    for (std::size_t r = 0; r < numRings; r++) {
+        const std::size_t ringEnd = fp.ringEnds[r];
+
+        // previous vertex (b), starting from the last point in the ring; carried
+        // across blocks so each block's first edge connects to the prior vertex
+        T bx = fp.coords[ringEnd - 2];
+        T by = fp.coords[ringEnd - 1];
+
+        for (std::size_t s = ringStart; s < ringEnd; s += stride, g += 4) {
+            std::size_t end = s + stride;
+            if (end > ringEnd) end = ringEnd;
+            const T bminX = fp.blocks[g], bminY = fp.blocks[g + 1],
+                    bmaxX = fp.blocks[g + 2], bmaxY = fp.blocks[g + 3];
+
+            // lower bound on the distance from (x, y) to any edge in this block
+            const T dx = x < bminX ? bminX - x : x > bmaxX ? x - bmaxX : T(0);
+            const T dy = y < bminY ? bminY - y : y > bmaxY ? y - bmaxY : T(0);
+            const bool skipDist = dx * dx + dy * dy >= minDistSq;
+
+            // this block's edges can only flip ray-cast parity if its bbox
+            // straddles y and extends right of x; else no edge crosses the
+            // rightward ray
+            const bool skipCross = y < bminY || y >= bmaxY || x > bmaxX;
+
+            if (skipDist && skipCross) {
+                bx = fp.coords[end - 2];
+                by = fp.coords[end - 1];
+                continue;
+            }
+
+            for (std::size_t i = s; i < end; i += 2) {
+                const T ax = fp.coords[i];
+                const T ay = fp.coords[i + 1];
+
+                if (!skipCross && (ay > y) != (by > y) &&
+                    (x < (bx - ax) * (y - ay) / (by - ay) + ax)) inside = !inside;
+
+                if (!skipDist) {
+                    const T distSq = getSegDistSq(x, y, ax, ay, bx, by);
+                    if (distSq < minDistSq) {
+                        minDistSq = distSq;
+                        cell.nsx1 = ax; cell.nsy1 = ay;
+                        cell.nsx2 = bx; cell.nsy2 = by;
+
+                        // the point is already close enough to the outline that
+                        // this cell can't possibly contain a better label
+                        // position — stop scanning
+                        if (minDistSq <= thresholdSq) return maxD;
+                    }
+                }
+
+                bx = ax;
+                by = ay;
+            }
+        }
+        ringStart = ringEnd;
+    }
+
+    return minDistSq == 0 ? T(0) : (inside ? T(1) : T(-1)) * std::sqrt(minDistSq);
+}
+
+// get polygon centroid (over the outer ring, coords[0..ringEnds[0]))
+template <class T>
+Cell<T> getCentroidCell(const FlatPolygon<T>& fp) {
     T area = 0;
-    geometry::point<T> c { 0, 0 };
-    const auto& ring = polygon.at(0);
-
-    for (std::size_t i = 0, len = ring.size(), j = len - 1; i < len; j = i++) {
-        const geometry::point<T>& a = ring[i];
-        const geometry::point<T>& b = ring[j];
-        auto f = a.x * b.y - b.x * a.y;
-        c.x += (a.x + b.x) * f;
-        c.y += (a.y + b.y) * f;
-        area += f * 3;
+    T x = 0;
+    T y = 0;
+    const std::size_t end = fp.ringEnds[0];
+
+    for (std::size_t i = 0, j = end - 2; i < end; j = i, i += 2) {
+        const T ax = fp.coords[i];
+        const T ay = fp.coords[i + 1];
+        const T bx = fp.coords[j];
+        const T by = fp.coords[j + 1];
+        const T f = ax * by - bx * ay;
+        x += (ax + bx) * f;
+        y += (ay + by) * f;
+        area += f * T(3);
     }
 
-    return Cell<T>(area == 0 ? ring.at(0) : c / area, 0, polygon);
+    const T negInf = -std::numeric_limits<T>::infinity();
+    if (area == 0) {
+        return Cell<T>(fp.coords[0], fp.coords[1], T(0), fp, negInf, nullptr);
+    }
+    Cell<T> centroid(x / area, y / area, T(0), fp, negInf, nullptr);
+    if (centroid.d < 0) {
+        return Cell<T>(fp.coords[0], fp.coords[1], T(0), fp, negInf, nullptr);
+    }
+    return centroid;
 }
 
 } // namespace detail
 
+// Find the pole of inaccessibility of `polygon` (the most distant internal
+// point from the polygon outline) within `precision`.
+//
+// If `distance` is non-null, it receives the signed distance from the returned
+// point to the polygon outline (positive inside, negative outside, 0 on the
+// boundary), matching the `.distance` property exposed by the v2.1.0 JS API.
 template <class T>
-geometry::point<T> polylabel(const geometry::polygon<T>& polygon, T precision = 1, bool debug = false) {
+geometry::point<T> polylabel(const geometry::polygon<T>& polygon, T precision = 1, bool debug = false, T* distance = nullptr) {
     using namespace detail;
 
     // find the bounding box of the outer ring
     const geometry::box<T> envelope = geometry::envelope(polygon.at(0));
+    const T minX = envelope.min.x;
+    const T minY = envelope.min.y;
+    const T maxX = envelope.max.x;
+    const T maxY = envelope.max.y;
+
+    const T width = maxX - minX;
+    const T height = maxY - minY;
+    const T cellSize = std::max(precision, std::min(width, height));
+
+    if (cellSize == precision) {
+        // degenerate / sub-precision polygon: no interior to search
+        if (distance) *distance = T(0);
+        return envelope.min;
+    }
 
-    const geometry::point<T> size {
-        envelope.max.x - envelope.min.x,
-        envelope.max.y - envelope.min.y
-    };
-
-    const T cellSize = std::min(size.x, size.y);
-    T h = cellSize / 2;
+    // flatten the polygon rings into a single contiguous coordinate buffer for
+    // cache-friendly, pointer-chase-free access in the hot distance loop
+    FlatPolygon<T> fp = flatten(polygon);
 
     // a priority queue of cells in order of their "potential" (max distance to polygon)
-    auto compareMax = [] (const Cell<T>& a, const Cell<T>& b) {
+    auto compareMax = [](const Cell<T>& a, const Cell<T>& b) {
         return a.max < b.max;
     };
     using Queue = std::priority_queue<Cell<T>, std::vector<Cell<T>>, decltype(compareMax)>;
     Queue cellQueue(compareMax);
 
-    if (cellSize == 0) {
-        return envelope.min;
-    }
-
-    // cover polygon with initial cells
-    for (T x = envelope.min.x; x < envelope.max.x; x += cellSize) {
-        for (T y = envelope.min.y; y < envelope.max.y; y += cellSize) {
-            cellQueue.push(Cell<T>({x + h, y + h}, h, polygon));
-        }
-    }
+    const T negInf = -std::numeric_limits<T>::infinity();
 
     // take centroid as the first best guess
-    auto bestCell = getCentroidCell(polygon);
+    Cell<T> bestCell = getCentroidCell(fp);
 
     // second guess: bounding box centroid
-    Cell<T> bboxCell(envelope.min + size / 2.0, 0, polygon);
-    if (bboxCell.d > bestCell.d) {
-        bestCell = bboxCell;
-    }
+    Cell<T> bboxCell(minX + width / T(2), minY + height / T(2), T(0), fp, negInf, nullptr);
+    if (bboxCell.d > bestCell.d) bestCell = bboxCell;
 
-    auto numProbes = cellQueue.size();
-    while (!cellQueue.empty()) {
-        // pick the most promising cell from the queue
-        auto cell = cellQueue.top();
-        cellQueue.pop();
+    std::size_t numProbes = 2;
+
+    auto potentiallyQueue = [&](T cx, T cy, T h, const Cell<T>* seed) {
+        // a cell is only useful if it can beat the best (d > bestCell.d) or is
+        // worth subdividing (max = d + h·√2 > bestCell.d + precision). Both
+        // fail once d ≤ threshold, so the distance scan can bail there early.
+        const T threshold = bestCell.d - std::max(T(0), h * std::sqrt(static_cast<T>(2)) - precision);
+        Cell<T> cell(cx, cy, h, fp, threshold, seed);
+        numProbes++;
+        if (cell.max > bestCell.d + precision) cellQueue.push(cell);
 
         // update the best cell if we found a better one
         if (cell.d > bestCell.d) {
             bestCell = cell;
-            if (debug) std::cout << "found best " << ::round(1e4 * cell.d) / 1e4 << " after " << numProbes << " probes" << std::endl;
+            if (debug) std::cout << "found best " << std::round(1e4 * cell.d) / 1e4 << " after " << numProbes << " probes" << std::endl;
+        }
+    };
+
+    // cover polygon with initial cells
+    T h = cellSize / T(2);
+    for (T x = minX; x < maxX; x += cellSize) {
+        for (T y = minY; y < maxY; y += cellSize) {
+            potentiallyQueue(x + h, y + h, h, nullptr);
         }
+    }
 
-        // do not drill down further if there's no chance of a better solution
-        if (cell.max - bestCell.d <= precision) continue;
+    while (!cellQueue.empty()) {
+        // pick the most promising cell from the queue
+        Cell<T> cell = cellQueue.top();
+        cellQueue.pop();
 
-        // split the cell into four cells
-        h = cell.h / 2;
-        cellQueue.push(Cell<T>({cell.c.x - h, cell.c.y - h}, h, polygon));
-        cellQueue.push(Cell<T>({cell.c.x + h, cell.c.y - h}, h, polygon));
-        cellQueue.push(Cell<T>({cell.c.x - h, cell.c.y + h}, h, polygon));
-        cellQueue.push(Cell<T>({cell.c.x + h, cell.c.y + h}, h, polygon));
-        numProbes += 4;
+        // do not drill down further if there's no chance of a better solution.
+        // because the queue is ordered by decreasing `max`, every subsequent
+        // cell is no better either, so we can stop entirely.
+        if (cell.max - bestCell.d <= precision) break;
+
+        // split the cell into four cells, seeding each with the parent's nearest segment
+        h = cell.h / T(2);
+        potentiallyQueue(cell.x - h, cell.y - h, h, &cell);
+        potentiallyQueue(cell.x + h, cell.y - h, h, &cell);
+        potentiallyQueue(cell.x - h, cell.y + h, h, &cell);
+        potentiallyQueue(cell.x + h, cell.y + h, h, &cell);
     }
 
     if (debug) {
@@ -172,7 +391,8 @@ geometry::point<T> polylabel(const geometry::polygon<T>& polygon, T precision =
         std::cout << "best distance: " << bestCell.d << std::endl;
     }
 
-    return bestCell.c;
+    if (distance) *distance = bestCell.d;
+    return geometry::point<T>{bestCell.x, bestCell.y};
 }
 
 } // namespace mapbox


=====================================
package-lock.json
=====================================
@@ -1,33 +1,36 @@
 {
   "name": "polylabel",
-  "version": "2.0.1",
+  "version": "2.1.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "polylabel",
-      "version": "2.0.1",
+      "version": "2.1.0",
       "license": "ISC",
       "dependencies": {
         "tinyqueue": "^3.0.0"
       },
       "devDependencies": {
-        "eslint": "^9.6.0",
-        "eslint-config-mourner": "^4.0.1"
+        "eslint": "^10.5.0",
+        "eslint-config-mourner": "^5.0.1"
       }
     },
     "node_modules/@eslint-community/eslint-utils": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
-      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+      "version": "4.9.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+      "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "eslint-visitor-keys": "^3.3.0"
+        "eslint-visitor-keys": "^3.4.3"
       },
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      },
       "peerDependencies": {
         "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
       }
@@ -46,9 +49,9 @@
       }
     },
     "node_modules/@eslint-community/regexpp": {
-      "version": "4.10.1",
-      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz",
-      "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==",
+      "version": "4.12.2",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+      "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -56,62 +59,127 @@
       }
     },
     "node_modules/@eslint/config-array": {
-      "version": "0.17.0",
-      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.0.tgz",
-      "integrity": "sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==",
+      "version": "0.23.5",
+      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+      "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
       "dev": true,
       "license": "Apache-2.0",
       "dependencies": {
-        "@eslint/object-schema": "^2.1.4",
+        "@eslint/object-schema": "^3.0.5",
         "debug": "^4.3.1",
-        "minimatch": "^3.1.2"
+        "minimatch": "^10.2.4"
       },
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       }
     },
-    "node_modules/@eslint/eslintrc": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
-      "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
+    "node_modules/@eslint/config-helpers": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
+      "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
       "dev": true,
-      "license": "MIT",
+      "license": "Apache-2.0",
       "dependencies": {
-        "ajv": "^6.12.4",
-        "debug": "^4.3.2",
-        "espree": "^10.0.1",
-        "globals": "^14.0.0",
-        "ignore": "^5.2.0",
-        "import-fresh": "^3.2.1",
-        "js-yaml": "^4.1.0",
-        "minimatch": "^3.1.2",
-        "strip-json-comments": "^3.1.1"
+        "@eslint/core": "^1.2.1"
       },
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      }
+    },
+    "node_modules/@eslint/core": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+      "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/json-schema": "^7.0.15"
       },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       }
     },
     "node_modules/@eslint/js": {
-      "version": "9.6.0",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz",
-      "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==",
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+      "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
       "dev": true,
       "license": "MIT",
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
+      },
+      "peerDependencies": {
+        "eslint": "^10.0.0"
+      },
+      "peerDependenciesMeta": {
+        "eslint": {
+          "optional": true
+        }
       }
     },
     "node_modules/@eslint/object-schema": {
-      "version": "2.1.4",
-      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
-      "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+      "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      }
+    },
+    "node_modules/@eslint/plugin-kit": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz",
+      "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/core": "^1.2.1",
+        "levn": "^0.4.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      }
+    },
+    "node_modules/@humanfs/core": {
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+      "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanfs/types": "^0.15.0"
+      },
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node": {
+      "version": "0.16.8",
+      "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+      "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanfs/core": "^0.19.2",
+        "@humanfs/types": "^0.15.0",
+        "@humanwhocodes/retry": "^0.4.0"
+      },
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/types": {
+      "version": "0.15.0",
+      "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+      "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18.0"
       }
     },
     "node_modules/@humanwhocodes/module-importer": {
@@ -129,9 +197,9 @@
       }
     },
     "node_modules/@humanwhocodes/retry": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
-      "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+      "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -142,78 +210,69 @@
         "url": "https://github.com/sponsors/nzakas"
       }
     },
-    "node_modules/@nodelib/fs.scandir": {
-      "version": "2.1.5",
-      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
-      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+    "node_modules/@stylistic/eslint-plugin": {
+      "version": "5.10.0",
+      "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz",
+      "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@nodelib/fs.stat": "2.0.5",
-        "run-parallel": "^1.1.9"
+        "@eslint-community/eslint-utils": "^4.9.1",
+        "@typescript-eslint/types": "^8.56.0",
+        "eslint-visitor-keys": "^4.2.1",
+        "espree": "^10.4.0",
+        "estraverse": "^5.3.0",
+        "picomatch": "^4.0.3"
       },
       "engines": {
-        "node": ">= 8"
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "peerDependencies": {
+        "eslint": "^9.0.0 || ^10.0.0"
       }
     },
-    "node_modules/@nodelib/fs.stat": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
-      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+    "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+      "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
       "dev": true,
-      "license": "MIT",
+      "license": "Apache-2.0",
       "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/@nodelib/fs.walk": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
-      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@nodelib/fs.scandir": "2.1.5",
-        "fastq": "^1.6.0"
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
-      "engines": {
-        "node": ">= 8"
+      "funding": {
+        "url": "https://opencollective.com/eslint"
       }
     },
-    "node_modules/@stylistic/eslint-plugin-js": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.3.0.tgz",
-      "integrity": "sha512-lQwoiYb0Fs6Yc5QS3uT8+T9CPKK2Eoxc3H8EnYJgM26v/DgtW+1lvy2WNgyBflU+ThShZaHm3a6CdD9QeKx23w==",
+    "node_modules/@stylistic/eslint-plugin/node_modules/espree": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+      "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
       "dev": true,
-      "license": "MIT",
+      "license": "BSD-2-Clause",
       "dependencies": {
-        "@types/eslint": "^8.56.10",
-        "acorn": "^8.11.3",
-        "eslint-visitor-keys": "^4.0.0",
-        "espree": "^10.0.1"
+        "acorn": "^8.15.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^4.2.1"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
-      "peerDependencies": {
-        "eslint": ">=8.40.0"
+      "funding": {
+        "url": "https://opencollective.com/eslint"
       }
     },
-    "node_modules/@types/eslint": {
-      "version": "8.56.10",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz",
-      "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==",
+    "node_modules/@types/esrecurse": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+      "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
       "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@types/estree": "*",
-        "@types/json-schema": "*"
-      }
+      "license": "MIT"
     },
     "node_modules/@types/estree": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
-      "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+      "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
       "dev": true,
       "license": "MIT"
     },
@@ -224,10 +283,24 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@typescript-eslint/types": {
+      "version": "8.62.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz",
+      "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
     "node_modules/acorn": {
-      "version": "8.12.0",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
-      "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
+      "version": "8.17.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+      "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
       "dev": true,
       "license": "MIT",
       "bin": {
@@ -248,9 +321,9 @@
       }
     },
     "node_modules/ajv": {
-      "version": "6.12.6",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "version": "6.15.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+      "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -264,115 +337,33 @@
         "url": "https://github.com/sponsors/epoberezkin"
       }
     },
-    "node_modules/ansi-regex": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
-      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/ansi-styles": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "color-convert": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/argparse": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-      "dev": true,
-      "license": "Python-2.0"
-    },
     "node_modules/balanced-match": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
-      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
       "dev": true,
       "license": "MIT",
-      "dependencies": {
-        "balanced-match": "^1.0.0",
-        "concat-map": "0.0.1"
-      }
-    },
-    "node_modules/callsites": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
-      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
       "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
+        "node": "18 || 20 || >=22"
       }
     },
-    "node_modules/color-convert": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+    "node_modules/brace-expansion": {
+      "version": "5.0.6",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+      "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "color-name": "~1.1.4"
+        "balanced-match": "^4.0.2"
       },
       "engines": {
-        "node": ">=7.0.0"
+        "node": "18 || 20 || >=22"
       }
     },
-    "node_modules/color-name": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/concat-map": {
-      "version": "0.0.1",
-      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
-      "dev": true,
-      "license": "MIT"
-    },
     "node_modules/cross-spawn": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
-      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -385,13 +376,13 @@
       }
     },
     "node_modules/debug": {
-      "version": "4.3.5",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
-      "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "ms": "2.1.2"
+        "ms": "^2.1.3"
       },
       "engines": {
         "node": ">=6.0"
@@ -423,29 +414,33 @@
       }
     },
     "node_modules/eslint": {
-      "version": "9.6.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.6.0.tgz",
-      "integrity": "sha512-ElQkdLMEEqQNM9Njff+2Y4q2afHk7JpkPvrd7Xh7xefwgQynqPxwf55J7di9+MEibWUGdNjFF9ITG9Pck5M84w==",
+      "version": "10.5.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz",
+      "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==",
       "dev": true,
       "license": "MIT",
+      "workspaces": [
+        "packages/*"
+      ],
       "dependencies": {
-        "@eslint-community/eslint-utils": "^4.2.0",
-        "@eslint-community/regexpp": "^4.6.1",
-        "@eslint/config-array": "^0.17.0",
-        "@eslint/eslintrc": "^3.1.0",
-        "@eslint/js": "9.6.0",
+        "@eslint-community/eslint-utils": "^4.8.0",
+        "@eslint-community/regexpp": "^4.12.2",
+        "@eslint/config-array": "^0.23.5",
+        "@eslint/config-helpers": "^0.6.0",
+        "@eslint/core": "^1.2.1",
+        "@eslint/plugin-kit": "^0.7.2",
+        "@humanfs/node": "^0.16.6",
         "@humanwhocodes/module-importer": "^1.0.1",
-        "@humanwhocodes/retry": "^0.3.0",
-        "@nodelib/fs.walk": "^1.2.8",
-        "ajv": "^6.12.4",
-        "chalk": "^4.0.0",
-        "cross-spawn": "^7.0.2",
+        "@humanwhocodes/retry": "^0.4.2",
+        "@types/estree": "^1.0.6",
+        "ajv": "^6.14.0",
+        "cross-spawn": "^7.0.6",
         "debug": "^4.3.2",
         "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^8.0.1",
-        "eslint-visitor-keys": "^4.0.0",
-        "espree": "^10.1.0",
-        "esquery": "^1.5.0",
+        "eslint-scope": "^9.1.2",
+        "eslint-visitor-keys": "^5.0.1",
+        "espree": "^11.2.0",
+        "esquery": "^1.7.0",
         "esutils": "^2.0.2",
         "fast-deep-equal": "^3.1.3",
         "file-entry-cache": "^8.0.0",
@@ -454,89 +449,95 @@
         "ignore": "^5.2.0",
         "imurmurhash": "^0.1.4",
         "is-glob": "^4.0.0",
-        "is-path-inside": "^3.0.3",
         "json-stable-stringify-without-jsonify": "^1.0.1",
-        "levn": "^0.4.1",
-        "lodash.merge": "^4.6.2",
-        "minimatch": "^3.1.2",
+        "minimatch": "^10.2.4",
         "natural-compare": "^1.4.0",
-        "optionator": "^0.9.3",
-        "strip-ansi": "^6.0.1",
-        "text-table": "^0.2.0"
+        "optionator": "^0.9.3"
       },
       "bin": {
         "eslint": "bin/eslint.js"
       },
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       },
       "funding": {
         "url": "https://eslint.org/donate"
+      },
+      "peerDependencies": {
+        "jiti": "*"
+      },
+      "peerDependenciesMeta": {
+        "jiti": {
+          "optional": true
+        }
       }
     },
     "node_modules/eslint-config-mourner": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/eslint-config-mourner/-/eslint-config-mourner-4.0.1.tgz",
-      "integrity": "sha512-seylu4qdUc7kabx42zEedJiCyRDYqwsZLFPPnMRntmOFOQv6cKoM+FqhYpLKZ5dF7A7wW6mCympQSkg6RVxGlg==",
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-config-mourner/-/eslint-config-mourner-5.0.1.tgz",
+      "integrity": "sha512-+bJjgJanAP1Nk0HIO1aV7CJZLa7zl/ymaFSff+HeSLHr9ifuHc8nX9dNdrjO7GnGdrgRMrmrAq3TCqfwj6NHiQ==",
       "dev": true,
       "license": "ISC",
       "dependencies": {
-        "@eslint/js": "^9.5.0",
-        "@stylistic/eslint-plugin-js": "^2.2.2"
+        "@eslint/js": "^10.0.1",
+        "@stylistic/eslint-plugin": "^5.10.0",
+        "globals": "^17.6.0"
       }
     },
     "node_modules/eslint-scope": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz",
-      "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==",
+      "version": "9.1.2",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+      "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
+        "@types/esrecurse": "^4.3.1",
+        "@types/estree": "^1.0.8",
         "esrecurse": "^4.3.0",
         "estraverse": "^5.2.0"
       },
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       },
       "funding": {
         "url": "https://opencollective.com/eslint"
       }
     },
     "node_modules/eslint-visitor-keys": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
-      "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+      "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       },
       "funding": {
         "url": "https://opencollective.com/eslint"
       }
     },
     "node_modules/espree": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
-      "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
+      "version": "11.2.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+      "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
-        "acorn": "^8.12.0",
+        "acorn": "^8.16.0",
         "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^4.0.0"
+        "eslint-visitor-keys": "^5.0.1"
       },
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       },
       "funding": {
         "url": "https://opencollective.com/eslint"
       }
     },
     "node_modules/esquery": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
-      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+      "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
       "dev": true,
       "license": "BSD-3-Clause",
       "dependencies": {
@@ -600,16 +601,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/fastq": {
-      "version": "1.17.1",
-      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
-      "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
-      "dev": true,
-      "license": "ISC",
-      "dependencies": {
-        "reusify": "^1.0.4"
-      }
-    },
     "node_modules/file-entry-cache": {
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -655,9 +646,9 @@
       }
     },
     "node_modules/flatted": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
-      "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+      "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
       "dev": true,
       "license": "ISC"
     },
@@ -675,9 +666,9 @@
       }
     },
     "node_modules/globals": {
-      "version": "14.0.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
-      "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+      "version": "17.7.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
+      "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -687,43 +678,16 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/ignore": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
-      "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
       "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 4"
       }
     },
-    "node_modules/import-fresh": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
-      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "parent-module": "^1.0.0",
-        "resolve-from": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/imurmurhash": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -757,16 +721,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/is-path-inside": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
-      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -774,19 +728,6 @@
       "dev": true,
       "license": "ISC"
     },
-    "node_modules/js-yaml": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "argparse": "^2.0.1"
-      },
-      "bin": {
-        "js-yaml": "bin/js-yaml.js"
-      }
-    },
     "node_modules/json-buffer": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -848,30 +789,26 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/lodash.merge": {
-      "version": "4.6.2",
-      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
-      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
-      "dev": true,
-      "license": "MIT"
-    },
     "node_modules/minimatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "version": "10.2.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+      "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
       "dev": true,
-      "license": "ISC",
+      "license": "BlueOak-1.0.0",
       "dependencies": {
-        "brace-expansion": "^1.1.7"
+        "brace-expansion": "^5.0.5"
       },
       "engines": {
-        "node": "*"
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
       }
     },
     "node_modules/ms": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
       "dev": true,
       "license": "MIT"
     },
@@ -932,19 +869,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/parent-module": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
-      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "callsites": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/path-exists": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -965,6 +889,19 @@
         "node": ">=8"
       }
     },
+    "node_modules/picomatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
     "node_modules/prelude-ls": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -985,72 +922,6 @@
         "node": ">=6"
       }
     },
-    "node_modules/queue-microtask": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
-      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "MIT"
-    },
-    "node_modules/resolve-from": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
-      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/reusify": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
-      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "iojs": ">=1.0.0",
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/run-parallel": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
-      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "MIT",
-      "dependencies": {
-        "queue-microtask": "^1.2.2"
-      }
-    },
     "node_modules/shebang-command": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -1074,52 +945,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/strip-ansi": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
-      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "ansi-regex": "^5.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/strip-json-comments": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
-      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/supports-color": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "has-flag": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/text-table": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
-      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
-      "dev": true,
-      "license": "MIT"
-    },
     "node_modules/tinyqueue": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",


=====================================
package.json
=====================================
@@ -1,6 +1,6 @@
 {
   "name": "polylabel",
-  "version": "2.0.1",
+  "version": "2.1.0",
   "description": "A JS library for finding optimal label position inside a polygon",
   "keywords": [
     "polygon",
@@ -14,12 +14,13 @@
     "tinyqueue": "^3.0.0"
   },
   "devDependencies": {
-    "eslint": "^9.6.0",
-    "eslint-config-mourner": "^4.0.1"
+    "eslint": "^10.5.0",
+    "eslint-config-mourner": "^5.0.1"
   },
   "scripts": {
     "pretest": "eslint *.js test/*.js",
-    "test": "node test/test.js"
+    "test": "node test/test.js",
+    "prepublishOnly": "npm test"
   },
   "files": [
     "include",
@@ -29,6 +30,6 @@
     "type": "git",
     "url": "https://github.com/mapbox/polylabel.git"
   },
-  "author": "Vladimir Agafonkin",
+  "author": "Volodymyr Agafonkin",
   "license": "ISC"
 }


=====================================
polylabel.js
=====================================
@@ -1,6 +1,9 @@
 
 import Queue from 'tinyqueue';
 
+// number of consecutive edges grouped under a single bounding box for block-skip
+const K = 32;
+
 export default function polylabel(polygon, precision = 1.0, debug = false) {
     // find the bounding box of the outer ring
     let minX = Infinity;
@@ -25,20 +28,41 @@ export default function polylabel(polygon, precision = 1.0, debug = false) {
         return result;
     }
 
+    // flatten the polygon rings into a single contiguous coordinate buffer for
+    // cache-friendly, pointer-chase-free access in the hot distance loop
+    let numPoints = 0;
+    for (const ring of polygon) numPoints += ring.length;
+    const coords = new Float64Array(numPoints * 2);
+    const ringEnds = []; // end offset into coords for each ring (start = previous end, or 0)
+    let c = 0;
+    for (const ring of polygon) {
+        for (let i = 0; i < ring.length; i++) {
+            coords[c++] = ring[i][0];
+            coords[c++] = ring[i][1];
+        }
+        ringEnds.push(c);
+    }
+
+    const blocks = buildBlocks(coords, ringEnds);
+
     // a priority queue of cells in order of their "potential" (max distance to polygon)
     const cellQueue = new Queue([], (a, b) => b.max - a.max);
 
     // take centroid as the first best guess
-    let bestCell = getCentroidCell(polygon);
+    let bestCell = getCentroidCell(coords, ringEnds, blocks);
 
     // second guess: bounding box centroid
-    const bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, polygon);
+    const bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, coords, ringEnds, blocks, -Infinity, null);
     if (bboxCell.d > bestCell.d) bestCell = bboxCell;
 
     let numProbes = 2;
 
-    function potentiallyQueue(x, y, h) {
-        const cell = new Cell(x, y, h, polygon);
+    function potentiallyQueue(x, y, h, seed) {
+        // a cell is only useful if it can beat the best (d > bestCell.d) or is
+        // worth subdividing (max = d + h·√2 > bestCell.d + precision). Both fail
+        // once d ≤ threshold, so the distance scan can bail there early.
+        const threshold = bestCell.d - Math.max(0, h * Math.SQRT2 - precision);
+        const cell = new Cell(x, y, h, coords, ringEnds, blocks, threshold, seed);
         numProbes++;
         if (cell.max > bestCell.d + precision) cellQueue.push(cell);
 
@@ -53,23 +77,23 @@ export default function polylabel(polygon, precision = 1.0, debug = false) {
     let h = cellSize / 2;
     for (let x = minX; x < maxX; x += cellSize) {
         for (let y = minY; y < maxY; y += cellSize) {
-            potentiallyQueue(x + h, y + h, h);
+            potentiallyQueue(x + h, y + h, h, null);
         }
     }
 
     while (cellQueue.length) {
         // pick the most promising cell from the queue
-        const {max, x, y, h: ch} = cellQueue.pop();
+        const cell = cellQueue.pop();
 
         // do not drill down further if there's no chance of a better solution
-        if (max - bestCell.d <= precision) break;
-
-        // split the cell into four cells
-        h = ch / 2;
-        potentiallyQueue(x - h, y - h, h);
-        potentiallyQueue(x + h, y - h, h);
-        potentiallyQueue(x - h, y + h, h);
-        potentiallyQueue(x + h, y + h, h);
+        if (cell.max - bestCell.d <= precision) break;
+
+        // split the cell into four cells, seeding each with the parent's nearest segment
+        h = cell.h / 2;
+        potentiallyQueue(cell.x - h, cell.y - h, h, cell);
+        potentiallyQueue(cell.x + h, cell.y - h, h, cell);
+        potentiallyQueue(cell.x - h, cell.y + h, h, cell);
+        potentiallyQueue(cell.x + h, cell.y + h, h, cell);
     }
 
     if (debug) {
@@ -81,67 +105,170 @@ export default function polylabel(polygon, precision = 1.0, debug = false) {
     return result;
 }
 
-function Cell(x, y, h, polygon) {
+function Cell(x, y, h, coords, ringEnds, blocks, maxD, seed) {
     this.x = x; // cell center x
     this.y = y; // cell center y
     this.h = h; // half the cell size
-    this.d = pointToPolygonDist(x, y, polygon); // distance from cell center to polygon
-    this.max = this.d + this.h * Math.SQRT2; // max distance to polygon within a cell
+    // nsx1..nsy2 hold the nearest segment found below, so child cells can seed
+    // their scan with it (a child is almost always nearest to the same segment)
+    this.nsx1 = 0; this.nsy1 = 0; this.nsx2 = 0; this.nsy2 = 0;
+    this.d = pointToPolygonDist(this, coords, ringEnds, blocks, maxD, seed); // distance from cell center to polygon
+    this.max = this.d + h * Math.SQRT2; // max distance to polygon within a cell
 }
 
-// signed distance from point to polygon outline (negative if point is outside)
-function pointToPolygonDist(x, y, polygon) {
+// signed distance from cell center to polygon outline (negative if outside),
+// also recording the nearest segment on the cell. maxD is a distance threshold:
+// if a partial result proves the center is no farther than maxD from the outline,
+// the scan bails out early and returns maxD, since the caller has already
+// determined such a cell can't beat the best. seed is the parent cell (or null);
+// its nearest segment is checked first so boundary cells reach the early-out
+// threshold without scanning the whole outline.
+function pointToPolygonDist(cell, coords, ringEnds, blocks, maxD, seed) {
+    const x = cell.x;
+    const y = cell.y;
     let inside = false;
     let minDistSq = Infinity;
+    const thresholdSq = maxD > 0 ? maxD * maxD : -1;
 
-    for (const ring of polygon) {
-        for (let i = 0, len = ring.length, j = len - 1; i < len; j = i++) {
-            const a = ring[i];
-            const b = ring[j];
+    if (seed !== null) {
+        cell.nsx1 = seed.nsx1; cell.nsy1 = seed.nsy1; cell.nsx2 = seed.nsx2; cell.nsy2 = seed.nsy2;
+        minDistSq = getSegDistSq(x, y, seed.nsx1, seed.nsy1, seed.nsx2, seed.nsy2);
+        if (minDistSq <= thresholdSq) return maxD;
+    }
+
+    const stride = K * 2;
+    const numRings = ringEnds.length;
+    let g = 0; // running block index into bboxes
+    let ringStart = 0;
+
+    for (let r = 0; r < numRings; r++) {
+        const ringEnd = ringEnds[r];
+
+        // previous vertex (b), starting from the last point in the ring; carried
+        // across blocks so each block's first edge connects to the prior vertex
+        let bx = coords[ringEnd - 2];
+        let by = coords[ringEnd - 1];
+
+        for (let s = ringStart; s < ringEnd; s += stride, g += 4) {
+            let end = s + stride;
+            if (end > ringEnd) end = ringEnd;
+            const bminX = blocks[g], bminY = blocks[g + 1], bmaxX = blocks[g + 2], bmaxY = blocks[g + 3];
+
+            // lower bound on the distance from (x, y) to any edge in this block
+            const dx = x < bminX ? bminX - x : x > bmaxX ? x - bmaxX : 0;
+            const dy = y < bminY ? bminY - y : y > bmaxY ? y - bmaxY : 0;
+            const skipDist = dx * dx + dy * dy >= minDistSq;
 
-            if ((a[1] > y !== b[1] > y) &&
-                (x < (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]) + a[0])) inside = !inside;
+            // this block's edges can only flip ray-cast parity if its bbox straddles
+            // y and extends right of x; else no edge crosses the rightward ray
+            const skipCross = y < bminY || y >= bmaxY || x > bmaxX;
 
-            minDistSq = Math.min(minDistSq, getSegDistSq(x, y, a, b));
+            if (skipDist && skipCross) {
+                bx = coords[end - 2];
+                by = coords[end - 1];
+                continue;
+            }
+
+            for (let i = s; i < end; i += 2) {
+                const ax = coords[i];
+                const ay = coords[i + 1];
+
+                if (!skipCross && (ay > y !== by > y) &&
+                    (x < (bx - ax) * (y - ay) / (by - ay) + ax)) inside = !inside;
+
+                if (!skipDist) {
+                    const distSq = getSegDistSq(x, y, ax, ay, bx, by);
+                    if (distSq < minDistSq) {
+                        minDistSq = distSq;
+                        cell.nsx1 = ax; cell.nsy1 = ay; cell.nsx2 = bx; cell.nsy2 = by;
+
+                        // the point is already close enough to the outline that this cell
+                        // can't possibly contain a better label position — stop scanning
+                        if (minDistSq <= thresholdSq) return maxD;
+                    }
+                }
+
+                bx = ax;
+                by = ay;
+            }
         }
+        ringStart = ringEnd;
     }
 
     return minDistSq === 0 ? 0 : (inside ? 1 : -1) * Math.sqrt(minDistSq);
 }
 
-// get polygon centroid
-function getCentroidCell(polygon) {
+// precompute one bounding box per block of K consecutive edges (over both
+// endpoints of every edge in it) so the distance scan can skip whole blocks in
+// O(1). The block layout mirrors the flattened coords/ringEnds and is re-derived
+// in the scan, so only the bboxes need storing: a flat [minX,minY,maxX,maxY] run
+// per block, sized upfront from the ring lengths.
+function buildBlocks(coords, ringEnds) {
+    const stride = K * 2;
+    let numBlocks = 0;
+    let ringStart = 0;
+    for (let r = 0; r < ringEnds.length; r++) {
+        numBlocks += Math.ceil((ringEnds[r] - ringStart) / stride);
+        ringStart = ringEnds[r];
+    }
+
+    const blocks = new Float64Array(numBlocks * 4);
+    let g = 0;
+    ringStart = 0;
+    for (let r = 0; r < ringEnds.length; r++) {
+        const ringEnd = ringEnds[r];
+        for (let s = ringStart; s < ringEnd; s += stride, g += 4) {
+            const end = s + stride < ringEnd ? s + stride : ringEnd;
+            const prev = s === ringStart ? ringEnd - 2 : s - 2;
+
+            let minX = coords[prev], minY = coords[prev + 1];
+            let maxX = minX, maxY = minY;
+            for (let i = s; i < end; i += 2) {
+                const px = coords[i], py = coords[i + 1];
+                if (px < minX) minX = px; else if (px > maxX) maxX = px;
+                if (py < minY) minY = py; else if (py > maxY) maxY = py;
+            }
+            blocks[g] = minX; blocks[g + 1] = minY; blocks[g + 2] = maxX; blocks[g + 3] = maxY;
+        }
+        ringStart = ringEnd;
+    }
+
+    return blocks;
+}
+
+// get polygon centroid (over the outer ring, coords[0..ringEnds[0]))
+function getCentroidCell(coords, ringEnds, blocks) {
     let area = 0;
     let x = 0;
     let y = 0;
-    const points = polygon[0];
-
-    for (let i = 0, len = points.length, j = len - 1; i < len; j = i++) {
-        const a = points[i];
-        const b = points[j];
-        const f = a[0] * b[1] - b[0] * a[1];
-        x += (a[0] + b[0]) * f;
-        y += (a[1] + b[1]) * f;
+    const end = ringEnds[0];
+
+    for (let i = 0, j = end - 2; i < end; j = i, i += 2) {
+        const ax = coords[i];
+        const ay = coords[i + 1];
+        const bx = coords[j];
+        const by = coords[j + 1];
+        const f = ax * by - bx * ay;
+        x += (ax + bx) * f;
+        y += (ay + by) * f;
         area += f * 3;
     }
-    const centroid = new Cell(x / area, y / area, 0, polygon);
-    if (area === 0 || centroid.d < 0) return new Cell(points[0][0], points[0][1], 0, polygon);
+    const centroid = new Cell(x / area, y / area, 0, coords, ringEnds, blocks, -Infinity, null);
+    if (area === 0 || centroid.d < 0) return new Cell(coords[0], coords[1], 0, coords, ringEnds, blocks, -Infinity, null);
     return centroid;
 }
 
 // get squared distance from a point to a segment
-function getSegDistSq(px, py, a, b) {
-    let x = a[0];
-    let y = a[1];
-    let dx = b[0] - x;
-    let dy = b[1] - y;
+function getSegDistSq(px, py, x, y, bx, by) {
+    let dx = bx - x;
+    let dy = by - y;
 
     if (dx !== 0 || dy !== 0) {
         const t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);
 
         if (t > 1) {
-            x = b[0];
-            y = b[1];
+            x = bx;
+            y = by;
 
         } else if (t > 0) {
             x += dx * t;


=====================================
test/test.cpp
=====================================
@@ -1,64 +1,106 @@
 #include <mapbox/polylabel.hpp>
 
-#include <rapidjson/document.h>
-#include <rapidjson/writer.h>
-#include <rapidjson/stringbuffer.h>
-
 #include <cassert>
+#include <cctype>
+#include <cstdlib>
 #include <fstream>
 #include <sstream>
-#include <iostream>
+#include <stdexcept>
+#include <string>
 
 using namespace mapbox;
 
-// Use the CrtAllocator, because the MemoryPoolAllocator is broken on ARM
-// https://github.com/miloyip/rapidjson/issues/200, 301, 388
-using rapidjson_allocator = rapidjson::CrtAllocator;
-using rapidjson_document = rapidjson::GenericDocument<rapidjson::UTF8<>, rapidjson_allocator>;
-using rapidjson_value = rapidjson::GenericValue<rapidjson::UTF8<>, rapidjson_allocator>;
+class JsonFixtureParser {
+public:
+    explicit JsonFixtureParser(std::string input_) : input(std::move(input_)) {}
 
-geometry::polygon<double> fixture(const std::string& path) {
-    std::ifstream t(path.c_str());
-    std::stringstream buffer;
-    buffer << t.rdbuf();
+    geometry::polygon<double> parsePolygon() {
+        geometry::polygon<double> polygon;
+        consume('[');
+        skipWhitespace();
+        while (!tryConsume(']')) {
+            polygon.push_back(parseRing());
+            skipWhitespace();
+            tryConsume(',');
+        }
+        return polygon;
+    }
 
-    rapidjson_document d;
-    d.Parse(buffer.str().c_str());
+private:
+    geometry::linear_ring<double> parseRing() {
+        geometry::linear_ring<double> ring;
+        consume('[');
+        skipWhitespace();
+        while (!tryConsume(']')) {
+            consume('[');
+            const double x = parseNumber();
+            consume(',');
+            const double y = parseNumber();
+            consume(']');
+            ring.push_back({x, y});
+            skipWhitespace();
+            tryConsume(',');
+        }
+        return ring;
+    }
 
-    geometry::polygon<double> result;
+    double parseNumber() {
+        skipWhitespace();
+        const char* begin = input.c_str() + pos;
+        char* end = nullptr;
+        const double value = std::strtod(begin, &end);
+        if (end == begin) throw std::runtime_error("expected number");
+        pos += static_cast<std::size_t>(end - begin);
+        skipWhitespace();
+        return value;
+    }
 
-    for (const auto& ringArray : d.GetArray()) {
-        geometry::linear_ring<double> ring;
-        for (const auto& point : ringArray.GetArray()) {
-            ring.push_back({
-                point[0].GetDouble(),
-                point[1].GetDouble()
-            });
+    void consume(char expected) {
+        skipWhitespace();
+        if (pos >= input.size() || input[pos] != expected) {
+            throw std::runtime_error(std::string("expected '") + expected + "'");
+        }
+        ++pos;
+        skipWhitespace();
+    }
+
+    bool tryConsume(char expected) {
+        skipWhitespace();
+        if (pos < input.size() && input[pos] == expected) {
+            ++pos;
+            skipWhitespace();
+            return true;
+        }
+        return false;
+    }
+
+    void skipWhitespace() {
+        while (pos < input.size() && std::isspace(static_cast<unsigned char>(input[pos]))) {
+            ++pos;
         }
-        result.push_back(std::move(ring));
     }
 
-    return result;
+    std::string input;
+    std::size_t pos = 0;
+};
+
+geometry::polygon<double> fixture(const std::string& path) {
+    std::ifstream file(path.c_str());
+    std::stringstream buffer;
+    buffer << file.rdbuf();
+    return JsonFixtureParser(buffer.str()).parsePolygon();
 }
 
 int main() {
-    geometry::polygon<double> water1 = fixture("./test/fixtures/water1.json");
-    geometry::polygon<double> water2 = fixture("./test/fixtures/water2.json");
+    const auto water1 = fixture("./test/fixtures/water1.json");
+    const auto water2 = fixture("./test/fixtures/water2.json");
 
-    // finds pole of inaccessibility for water1 and precision 1
     assert(polylabel(water1, 1.0) == geometry::point<double>(3865.85009765625, 2124.87841796875));
-
-    // finds pole of inaccessibility for water1 and precision 50
     assert(polylabel(water1, 50.0) == geometry::point<double>(3854.296875, 2123.828125));
-
-    // finds pole of inaccessibility for water2 and default precision 1
     assert(polylabel(water2) == geometry::point<double>(3263.5, 3263.5));
 
-    // works on degenerate polygons
-    assert(polylabel(geometry::polygon<double>({{{0, 0}, {1, 0}, {2, 0}, {0, 0}}}))
-        == geometry::point<double>(0, 0));
-    assert(polylabel(geometry::polygon<double>({{{0, 0}, {1, 0}, {1, 1}, {1, 0}, {0, 0}}}))
-        == geometry::point<double>(0, 0));
-
-    return 0;
+    assert(polylabel(geometry::polygon<double>({{{0, 0}, {1, 0}, {2, 0}, {0, 0}}})) ==
+           geometry::point<double>(0, 0));
+    assert(polylabel(geometry::polygon<double>({{{0, 0}, {1, 0}, {1, 1}, {1, 0}, {0, 0}}})) ==
+           geometry::point<double>(0, 0));
 }


=====================================
test/test.js
=====================================
@@ -1,10 +1,8 @@
 import polylabel from '../polylabel.js';
 import test from 'node:test';
 import assert from 'node:assert/strict';
-import {readFileSync} from 'fs';
-
-const water1 = JSON.parse(readFileSync(new URL('fixtures/water1.json', import.meta.url)));
-const water2 = JSON.parse(readFileSync(new URL('fixtures/water2.json', import.meta.url)));
+import water1 from './fixtures/water1.json' with {type: 'json'};
+import water2 from './fixtures/water2.json' with {type: 'json'};
 
 test('finds pole of inaccessibility for water1 and precision 1', () => {
     const p = polylabel(water1, 1);



View it on GitLab: https://salsa.debian.org/debian-gis-team/mapbox-polylabel/-/compare/2373c8e59aff4e3f56bde7f763a692f958d48469...ea7d8ac5ef41853bdae0200e2da9b9d956cac378

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/mapbox-polylabel/-/compare/2373c8e59aff4e3f56bde7f763a692f958d48469...ea7d8ac5ef41853bdae0200e2da9b9d956cac378
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20260703/18550e06/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list