Skip to main content

Geo Polygonize

A native Rust port of the JTS/GEOS polygonization algorithm. This crate allows you to reconstruct valid polygons from a set of lines, including handling of complex topologies like holes, nested shells, and disconnected components.

Ask DeepWiki

Features

  • Robust Polygonization: Extracts polygons from unstructured linework.
  • Iterative Grid Noding (Unchecked): Splits and snaps dirty linework, without claiming certified snap-rounding guarantees.
  • Certified Fixed-Precision Noding: Optional hot-pixel snap rounding with an independent full-noding postcondition check.
  • Hardware Acceleration: Uses SIMD instructions (via wide crate) for critical geometric predicates like Point-in-Polygon checks.
  • Wasm Optimized: Tailored for WebAssembly with talc allocator and binary GeoArrow support.
  • Performance: SIMD, spatial indexing, and optional parallel execution, with checked-in benchmark tooling.
  • Geo Ecosystem: Fully integrated with geo-types and geo crates.
  • GeoArrow Support: Arrow C Data Interface and Arrow IPC integration with GeoArrow metadata.

Engineering Roadmap

For delivered foundations and the next evidence-gated work, see ROADMAP.md.

Usage

Library

use geo_polygonize_core::{polygonize_line_strings, PolygonizerOptions};
use geo_types::LineString;

fn main() {
    let ring = LineString::from(vec![
        (0.0, 0.0),
        (10.0, 0.0),
        (10.0, 10.0),
        (0.0, 10.0),
        (0.0, 0.0),
    ]);

    let result = polygonize_line_strings([&ring], &PolygonizerOptions::default())
        .expect("Polygonization failed");
    let polygons = result.into_multi_polygon();

    for polygon in polygons {
        println!("Found polygon: {polygon:?}");
    }
}

polygonize_line_strings accepts owned or borrowed geo_traits::LineStringTrait values. Use polygonize when source IDs or Z values must be supplied explicitly, and keep its full result when dangles, cut edges, invalid rings, or diagnostics are needed.

Rust consumers should import the supported facade from the crate root. Graph, noding, containment, utility, mutable-builder, and tiled APIs are internal or experimental and may change without compatibility guarantees before 1.0. TiledPolygonizer validates its grid/options and propagates per-tile errors, but callers must still choose a sufficient buffer and verify untiled equivalence for their workload. Its result includes per-tile topology counts and merge/dedup counts; these are observational and do not certify buffer sufficiency. Canonical mode applies the same final ordering pass after tile merge; the equivalence gate covers one, two, four, and many boundary crossings plus concave, hole, dirty-intersection, and dangle cases.

Choosing noding and precision

Polygonization quality is heavily influenced by input noding strategy.

  • node_input = false (default): Fastest path. Use this when your input linework is already noded (all intersections are explicit vertices).
  • node_input = true: Enables unchecked iterative grid noding. Use this for real-world datasets that may contain slight misalignments, overlaps, or self-intersections, and validate outputs when correctness must be certified.
  • PrecisionModel::Floating (default) preserves input coordinates and uses floating-point intersections.
  • PrecisionModel::FixedGrid { grid_size } rounds topology coordinates to an explicit positive grid, even when input noding is disabled. Choose the grid in the units of your data; oversnapping can collapse narrow features.

Practical workflow:

  1. Run with node_input = false first on trusted data.
  2. If you observe missing polygons, sliver artifacts, or unresolved intersections, enable node_input.
  3. Use a fixed precision model only when your application has an explicit coordinate grid contract.

Canonical options now use precision_model instead of snap_grid_size. Legacy positional Python, Wasm, and C APIs still translate their grid argument when noding is enabled and ignore it on the non-noding fast path.

Set options.noding.guarantee to NodingGuarantee::Validate to run an independent full-noding check before graph construction. Validation reports the first pair with an interior intersection or unnormalized collinear overlap. Use NodingGuarantee::CertifiedFixedPrecision with node_input, FixedGrid, the Snap backend, and SnapStrategy::Grid for hot-pixel snap rounding plus that validation. Certified coordinates must fit exact integer grid indices.

Z semantics

Topology is always computed in XY. ZPolicy::InterpolateAlongEdge is the default and reconstructs split-vertex Z on each source edge; PreferNearestEndpoint uses the nearer endpoint, and Ignore emits zero Z. When several source edges meet at the same XY node, the graph chooses the Z from the lowest line ID (then the lowest Z) and reports conflicts through diagnostics. ErrorOnConflict instead returns a typed error when the difference exceeds z.conflict_tolerance.

Use Line3D or the Python/Wasm typed-buffer APIs with stride=3 for Z-aware processing. The GeoRust, GeoJSON, and current GeoArrow adapters are XY-only.

Output semantics

The polygonizer intentionally returns only valid polygonal areas that can be formed from closed cycles:

  • Dangles are removed: dead-end edges do not appear in output polygons.
  • Cut edges are excluded: edges that are connected but cannot bound a face are ignored.
  • Holes and nested shells are preserved when enough boundary information is present.

This behavior matches classical JTS/GEOS polygonization semantics and is useful for cleaning linework before area analysis.

GeoArrow Integration

Rust Arrow and GeoArrow adapters live in the geo-polygonize-arrow crate; the Wasm crate includes it automatically.

For the Arrow C Data Interface ABI, including ownership and error retrieval, see docs/C_ABI.md.

use geo_polygonize_arrow::{polygonize_arrow, PolygonizerOptions};
// ... create Arrow array ...
// let result = polygonize_arrow(&array, &field, options);

The FlatGeoBuf file adapter lives in the geo-polygonize-flatgeobuf crate.

Python

The Python package is published as geo-polygonize-py and imported as geo_polygonize.

pip install geo-polygonize-py
import numpy as np
from geo_polygonize import polygonize, import_probe

# 1. Using Shapely LineStrings or coordinate lists directly
lines = [
    [(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)],
    [(0, 0), (10, 10)]
]

# return_polygons=True returns a list of shapely.geometry.Polygon objects
polygons = polygonize(lines=lines, return_polygons=True)
for p in polygons:
    print(p.area)

# 2. Using High-Performance Flat Arrays
# Flat buffers avoid Python object-per-coordinate overhead
coords = np.array([
    0.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0, 10.0, 0.0, 0.0,
    0.0, 0.0, 10.0, 10.0
], dtype=np.float64)

# Start indices for each line segment.
# The final closing offset is computed implicitly.
offsets = np.array([0, 5], dtype=np.uint32)

# Returns a stable dictionary with 'polygons', diagnostics, and provenance.
result_dict = polygonize(coords=coords, offsets=offsets)

# Native-extension probes are cheap and safe for optional integrations.
ok, error = import_probe()

CFB/autograder integrations should use the versioned production profile rather than assembling caller-side knobs or using legacy polygonize(..., node=True, snap=0.5) calls:

from geo_polygonize import cfb_robust_options, polygonize_with_options

result = polygonize_with_options(
    coords=coords,
    offsets=offsets,
    options=cfb_robust_options(),
)

The default return shape is a stable dictionary with polygons as SimplePolygon values. Use return_polygons=True only when you want Shapely Polygon objects.

When provenance is enabled, coincident and partially overlapping input lines are dissolved into one topology edge while every contributing nonzero line_id remains in the polygon's sorted boundary_line_ids.

SnapStrategy::Grid keeps topology and output coordinates on a configured fixed precision grid. The CFB profile uses GeosCompat: the grid establishes robust topology, then output nodes regain deterministic source XY coordinates to better match Shapely snap plus full-precision noding. It is not set_precision emulation, and exact parity is not guaranteed for many-to-one snaps.

For Shapely parity checks, compare report-mode outputs with the built-in mismatch helper:

from geo_polygonize import explain_mismatch, polygonize_with_options

options = cfb_robust_options()
result_a = polygonize_with_options(coords=coords_a, offsets=offsets_a, options=options)
result_b = polygonize_with_options(coords=coords_b, offsets=offsets_b, options=options)
result_a["options"] = options
result_b["options"] = options

mismatch = explain_mismatch(result_a, result_b)

For a minimal Shapely smoke comparison, use area signatures:

from shapely.ops import polygonize as shapely_polygonize

rust_polys = polygonize_with_options(lines=lines, options=cfb_robust_options(), return_polygons=True)
rust_areas = sorted(round(poly.area, 6) for poly in rust_polys)
shapely_areas = sorted(round(poly.area, 6) for poly in shapely_polygonize(lines))

WebAssembly (WASM)

This library supports WebAssembly with an ergonomic dual-build configuration that automatically utilizes SIMD instructions where available.

Installation:

npm install geo-polygonize

Standard Usage (Quick Demos): The default entry point automatically handles feature detection (SIMD) and lazy-loading of the Wasm binary. The Wasm is inlined as a Base64 Data URI, so no extra bundler configuration is needed. For app builds, prefer the slim entry point below so your bundler keeps the Wasm assets out of the JavaScript chunk.

import init, { polygonize, polygonize_geoarrow } from "geo-polygonize";

async function run() {
    await init();

    const geojson = {
        "type": "FeatureCollection",
        "features": [
            // ... your line features
        ]
    };

    // Returns a GeoJSON FeatureCollection string
    // Pass explicitly matching backend configuration if desired
    const result = polygonize(
        JSON.stringify(geojson),
        true, // node_input
        0.5   // snap_grid_size
    );
    console.log(JSON.parse(result));

    // Or use Arrow IPC bytes
    // const ipcBuffer = ...;
    // const arrowResult = polygonize_geoarrow(ipcBuffer, false, 1e-10, false);
}

Slim Usage (Apps / Manual Loading): For Vite and other app bundlers, import from geo-polygonize/slim and pass explicit Wasm asset URLs.

import { cfbRobustOptions, initBest } from "geo-polygonize/slim";
import scalarUrl from "geo-polygonize/geo_polygonize.wasm?url";
import simdUrl from "geo-polygonize/geo_polygonize_simd.wasm?url";

async function run() {
    const wasm = await initBest(
        { module_or_path: scalarUrl },
        { module_or_path: simdUrl },
    );

    const result = wasm.polygonizeWithOptions(
        JSON.stringify(geojson),
        cfbRobustOptions,
    );
}

For cancellable browser work, use the worker-backed asynchronous entry point. Aborting terminates its disposable worker; the direct Wasm exports above remain synchronous and cannot be interrupted with an AbortSignal.

import { polygonizeWithOptionsAsync } from "geo-polygonize";

const controller = new AbortController();
const result = await polygonizeWithOptionsAsync(
    JSON.stringify(geojson),
    cfbRobustOptions,
    { signal: controller.signal },
);
// controller.abort();

Multithreaded Usage (Experimental): This library provides a multithreaded build powered by wasm-bindgen-rayon.

import init, { initThreadPool, polygonize } from "geo-polygonize/threads";

async function run() {
    await init();

    // Initialize thread pool (e.g., with navigator.hardwareConcurrency)
    await initThreadPool(navigator.hardwareConcurrency);

    // ... use polygonize as usual
}

Important: Multithreaded WebAssembly requires SharedArrayBuffer, which is only available in secure contexts. You must serve your page with the following headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

CLI Example

The repository includes a CLI tool to polygonize GeoJSON files.

# Build the example
cargo build -p geo-polygonize-core --example polygonize --release

# Run on input lines
cargo run -p geo-polygonize-core --release --example polygonize -- --input lines.geojson --output polygons.geojson --node

Visualization

You can visualize the results using the provided Python script (requires matplotlib and shapely).

python3 scripts/visualize.py --input lines.geojson --output polygons.geojson --save result.png

Examples

Below are some examples of what the polygonizer can do.

Nested Holes and Islands

The algorithm correctly identifies nested structures (Island inside a Hole inside a Shell).

Nested Holes

Incomplete Grid / Dangles

The algorithm prunes dangles (dead-end lines) and extracts only closed cycles.

Incomplete Grid

Touching Polygons (Shared Edges)

Using robust noding (--node), it can reconstruct adjacent polygons that share boundaries, even if the input lines are not perfectly noded.

Touching Polygons

Self-Intersecting Geometry (Bowtie)

Self-intersecting lines are split at intersection points, and valid cycles are extracted.

Bowtie

Complex Geometries

The polygonizer can handle complex, curved inputs (approximated by LineStrings) such as overlapping circles and shapes with multiple holes.

Overlapping Circles: Note how the intersection regions are correctly identified as separate polygons.

Overlapping Circles

Curved Holes: A complex polygon with multiple circular holes.

Curved Holes

Benchmarks

This library includes a "severe" comparison suite against shapely (GEOS).

See BENCHMARKS.md for detailed results and instructions on how to run them.

Architecture

This implementation moves away from the pointer-based graph structures of JTS/GEOS to a Rust-idiomatic Index Graph (Arena) approach.

See ARCHITECTURE.md for a deep dive into the optimization strategies.

Key optimizations include:

  1. Noding: Unchecked iterative grid noding with spatially dispatched intersection detection.
  2. Vectorization: SIMD-accelerated Ray Casting for efficient Hole Assignment.
  3. Memory Layout: Structure of Arrays (SoA) for graph nodes and talc allocator for Wasm.

License

MIT/Apache-2.0

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

geo_polygonize_py-0.70.0.tar.gz (168.9 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

geo_polygonize_py-0.70.0-cp38-abi3-win_amd64.whl (683.0 kB view details)

Uploaded CPython 3.8+Windows x86-64

geo_polygonize_py-0.70.0-cp38-abi3-manylinux_2_35_x86_64.whl (810.7 kB view details)

Uploaded CPython 3.8+manylinux: glibc 2.35+ x86-64

geo_polygonize_py-0.70.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (763.8 kB view details)

Uploaded CPython 3.8+manylinux: glibc 2.17+ ARM64

geo_polygonize_py-0.70.0-cp38-abi3-macosx_11_0_arm64.whl (695.9 kB view details)

Uploaded CPython 3.8+macOS 11.0+ ARM64

File details

Details for the file geo_polygonize_py-0.70.0.tar.gz.

File metadata

  • Download URL: geo_polygonize_py-0.70.0.tar.gz
  • Upload date:
  • Size: 168.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for geo_polygonize_py-0.70.0.tar.gz
Algorithm Hash digest
SHA256 33e6da8ba38bacac6c1e7a638b969e4f787eb3bd91f45a629c2196515260cae0
MD5 3f53e2a9f1bca84e354b4cd97a166695
BLAKE2b-256 59d00f7c5a343191d031d3c51fe6c592a3c3ade04771c623f6a70b434a81e781

See more details on using hashes here.

Provenance

The following attestation bundles were made for geo_polygonize_py-0.70.0.tar.gz:

Publisher: publish-python.yml on graydonpleasants/geo-polygonize

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file geo_polygonize_py-0.70.0-cp38-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for geo_polygonize_py-0.70.0-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 a2e2764252934cbe032372517f6c93d3b0d5e8ea1ae776b4e31fbb7be8432c80
MD5 50828c217555dfae4fad53317f644271
BLAKE2b-256 f45a65d4a34d409f47f8adb6e35a9bf1e56ba14f5ec3788da08de439bd22d6bb

See more details on using hashes here.

Provenance

The following attestation bundles were made for geo_polygonize_py-0.70.0-cp38-abi3-win_amd64.whl:

Publisher: publish-python.yml on graydonpleasants/geo-polygonize

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file geo_polygonize_py-0.70.0-cp38-abi3-manylinux_2_35_x86_64.whl.

File metadata

File hashes

Hashes for geo_polygonize_py-0.70.0-cp38-abi3-manylinux_2_35_x86_64.whl
Algorithm Hash digest
SHA256 55a2dcc7912357d7d9291e0a140fc8a325766a3fccb2e5190057ae4333114008
MD5 11dd68505fb5fb5547ca03ac695f6629
BLAKE2b-256 163b882a171e63dd63ae9a28829bb4a30c4d311b2202e1a4154b6fe5eb5eb19a

See more details on using hashes here.

Provenance

The following attestation bundles were made for geo_polygonize_py-0.70.0-cp38-abi3-manylinux_2_35_x86_64.whl:

Publisher: publish-python.yml on graydonpleasants/geo-polygonize

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file geo_polygonize_py-0.70.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for geo_polygonize_py-0.70.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 4a0a8e0d084be9134bb125db7d9188bf593990aaf5a17d9d9dc28d090ef4ab79
MD5 ed32cf660ff5ec59493d6831a7a36121
BLAKE2b-256 20fb19bba60d06c7c217f4f59cf0e23396ad179ad61d66df1b114d0f4f124620

See more details on using hashes here.

Provenance

The following attestation bundles were made for geo_polygonize_py-0.70.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: publish-python.yml on graydonpleasants/geo-polygonize

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file geo_polygonize_py-0.70.0-cp38-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for geo_polygonize_py-0.70.0-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 acedfccd30684534e24999dbe9e097c5b54f14878f915574a6761ec4efed07d6
MD5 2969e6f7cc09f7a0f80d6f00a1e1e2e4
BLAKE2b-256 922724338bd5cb55d7f13739944ebc94e2a70677ebe4b7eeb938cb26a16da7f7

See more details on using hashes here.

Provenance

The following attestation bundles were made for geo_polygonize_py-0.70.0-cp38-abi3-macosx_11_0_arm64.whl:

Publisher: publish-python.yml on graydonpleasants/geo-polygonize

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.1.0

5 files

1.0.0

5 files

0.76.2

5 files

0.76.1

5 files

0.76.0

5 files

0.75.0

5 files

0.74.0

5 files

0.73.0

5 files

0.72.0

5 files

0.71.0

5 files

This release

0.70.0 This release

5 files

0.69.0

5 files

0.68.0

5 files

0.67.0

5 files

0.66.0

5 files

0.65.0

5 files

0.64.0

5 files

0.63.0

5 files

0.62.0

5 files

0.61.0

5 files

0.60.0

5 files

0.59.0

5 files

0.58.1

5 files

0.58.0

5 files

0.57.0

5 files

0.56.0

5 files

0.55.0

5 files

0.54.0

5 files

0.53.0

5 files

0.52.0

5 files

0.51.2

5 files

0.51.1

5 files

0.51.0

5 files

0.50.0

5 files

0.49.0

5 files

0.48.0

5 files

0.47.1

5 files

0.47.0

5 files

0.46.2

5 files

0.46.1

5 files

0.46.0

5 files

0.45.1

5 files

0.45.0

5 files

0.44.0

5 files

0.43.0

5 files

0.42.0

5 files

0.41.0

5 files

0.40.1

5 files

0.40.0

5 files

0.39.13

5 files

0.39.12

5 files

0.39.11

5 files

0.39.10

5 files

0.39.9

5 files

0.39.8

5 files

0.39.7

5 files

0.39.6

5 files

0.39.5

5 files

0.39.4

5 files

0.39.3

5 files

0.39.2

5 files

0.39.1

5 files

0.39.0

5 files

0.38.1

5 files

0.38.0

5 files

0.37.8

5 files

0.37.7

5 files

0.37.6

5 files

0.37.5

5 files

0.37.4

5 files

0.37.3

5 files

0.37.2

5 files

0.37.1

5 files

0.37.0

5 files

0.36.2

5 files

0.36.1

5 files

0.36.0

5 files

0.35.2

5 files

0.35.1

4 files

0.35.0

4 files

0.34.0

3 files

0.33.1

3 files

0.33.0

3 files

0.23.1

3 files

0.23.0

3 files

0.22.1

3 files

0.22.0

3 files

0.21.0

3 files

0.20.0

3 files

0.19.0

3 files

0.18.1

3 files

0.18.0

3 files

0.17.4

3 files

0.17.3

3 files

0.17.2

3 files

0.17.1

3 files

0.17.0

3 files

0.16.0

3 files

0.15.0

3 files

0.14.1

3 files

0.14.0

3 files

0.13.0

3 files

0.12.1

3 files

0.12.0

3 files

0.11.0

3 files

0.10.0

3 files

0.9.0

3 files

0.8.1

3 files

0.8.0

3 files

0.7.0

3 files

0.6.3

3 files

0.6.2

3 files

0.6.1

3 files

0.6.0

3 files

0.5.0

3 files

0.4.2

3 files

0.4.1

3 files

0.1.0

5 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page