Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ avoid adding features or APIs which do not map onto the

## Unreleased

## [4.5.0a2] - 2026-02-27

- `cells_to_h3shape`/`cells_to_geo` now handle all valid cell sets, including
sets that would produce global polygons crossing the antimeridian, poles, or are larger than a hemisphere
- Duplicate cells now raise `H3DuplicateInputError`
- Mixed-resolution cells now raise `H3ResMismatchError`
- Internal: switch from linked-list to array-based C API for cells-to-polygon conversion

## [4.5.0a1] - 2026-02-25

- Update `h3lib` with draft v4.5 changes
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = 'scikit_build_core.build'

[project]
name = 'h3'
version = '4.5.0a1'
version = '4.5.0a2'
description = "Uber's hierarchical hexagonal geospatial indexing system"
readme = 'readme.md'
license = {file = 'LICENSE'}
Expand Down
25 changes: 5 additions & 20 deletions src/h3/_cy/h3lib.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,6 @@ cdef extern from 'h3api.h':
int i
int j

ctypedef struct LinkedLatLng:
LatLng data 'vertex'
LinkedLatLng *next

# renaming these for clarity
ctypedef struct LinkedGeoLoop:
LinkedLatLng *data 'first'
LinkedLatLng *_data_last 'last' # not needed in Cython bindings
LinkedGeoLoop *next

ctypedef struct LinkedGeoPolygon:
LinkedGeoLoop *data 'first'
LinkedGeoLoop *_data_last 'last' # not needed in Cython bindings
LinkedGeoPolygon *next

ctypedef struct GeoLoop:
int numVerts
LatLng *verts
Expand Down Expand Up @@ -171,18 +156,18 @@ cdef extern from 'h3api.h':
double greatCircleDistanceKm(const LatLng *a, const LatLng *b) nogil
double greatCircleDistanceM(const LatLng *a, const LatLng *b) nogil

H3Error cellsToLinkedMultiPolygon(const H3int *h3Set, const int numCells, LinkedGeoPolygon *out)
void destroyLinkedMultiPolygon(LinkedGeoPolygon *polygon)
H3Error cellsToMultiPolygon(const H3int *cells, const int64_t numCells, GeoMultiPolygon *out)
void destroyGeoMultiPolygon(GeoMultiPolygon *mpoly)

H3Error maxPolygonToCellsSize(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t *count)
H3Error polygonToCells(const GeoPolygon *geoPolygon, int res, uint32_t flags, H3int *out)

H3Error maxPolygonToCellsSizeExperimental(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t *count)
H3Error polygonToCellsExperimental(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t sz, H3int *out)

# ctypedef struct GeoMultiPolygon:
# int numPolygons
# GeoPolygon *polygons
ctypedef struct GeoMultiPolygon:
int numPolygons
GeoPolygon *polygons

# int hexRange(H3int origin, int k, H3int *out)

Expand Down
58 changes: 20 additions & 38 deletions src/h3/_cy/to_multipoly.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -4,60 +4,42 @@ from .util cimport check_cell, coord2deg
from .error_system cimport check_for_error


# todo: it's driving me crazy that these three functions are all essentially the same linked list walker...
# grumble: no way to do iterators in with cdef functions!
cdef walk_polys(const h3lib.LinkedGeoPolygon* L):
out = []
while L:
out += [walk_loops(L.data)]
L = L.next
cdef _loop_to_list(const h3lib.GeoLoop *loop):
return [coord2deg(loop.verts[v]) for v in range(loop.numVerts)]

return out


cdef walk_loops(const h3lib.LinkedGeoLoop* L):
out = []
while L:
out += [walk_coords(L.data)]
L = L.next

return out

cdef _poly_to_lists(const h3lib.GeoPolygon *poly):
return (
[_loop_to_list(&poly.geoloop)]
+ [_loop_to_list(&poly.holes[h]) for h in range(poly.numHoles)]
)

cdef walk_coords(const h3lib.LinkedLatLng* L):
out = []
while L:
out += [coord2deg(L.data)]
L = L.next

return out

# todo: tuples instead of lists?
def _to_multi_polygon(const H3int[:] cells):
cdef:
h3lib.LinkedGeoPolygon polygon
h3lib.GeoMultiPolygon mpoly
H3int cell

for h in cells:
check_cell(h)
for cell in cells:
check_cell(cell)

check_for_error(
h3lib.cellsToLinkedMultiPolygon(&cells[0], len(cells), &polygon)
h3lib.cellsToMultiPolygon(&cells[0], len(cells), &mpoly)
)

out = walk_polys(&polygon)

# we're still responsible for cleaning up the passed in `polygon`,
# but not a problem here, since it is stack allocated
h3lib.destroyLinkedMultiPolygon(&polygon)
try:
out = [
_poly_to_lists(&mpoly.polygons[p])
for p in range(mpoly.numPolygons)
]
finally:
h3lib.destroyGeoMultiPolygon(&mpoly)

return out


def cells_to_multi_polygon(const H3int[:] cells):
# todo: gotta be a more elegant way to handle these...
if len(cells) == 0:
return []

multipoly = _to_multi_polygon(cells)

return multipoly
return _to_multi_polygon(cells)
Loading