                        Changes in version 0.9.0                        

Printing and documentation

  - A grid prints as one line (<HexGridInfo> aperture 3,
    resolution 10: 590,492 cells of 863.8 km^2) and gridded data as two;
    summary() keeps the full listing, which for gridded data now
    includes the grid's orientation and ellipsoid (#108). The list
    hexify_grid() builds and the dggs list as_dggrid() returns print as
    one line too.

  - Each class has one help page listing its methods: ?HexGridInfo-class
    and ?HexData-class carry show(), summary(), plot(), st_as_sf(),
    st_crs(), as_tibble() and the accessors (#110). Help titles are in
    Title Case. The empty pages hexify-conversions, hexify-grid and
    hexify-stats are gone, and hexify_grid(), dgverify(),
    hexify_compare_resolutions(), hexify_roundtrip_test() and the
    projection-level helpers (hexify_forward(), hexify_inverse(),
    hexify_forward_to_face(), hexify_which_face(),
    hexify_face_centers(), hexify_build_icosa()) are marked internal:
    hex_grid() is the grid constructor.

  - Every exported user-facing topic has a runnable example; the
    hex_globe_png() example runs in an interactive session with hexglobe
    installed (#109).

  - hex_grid(area_km2 = , type = "h3") no longer warns on every call:
    the note that H3 cells are not equal-area is one message per session
    on both the area and the resolution path (#112). grid_clip(),
    hexify_heatmap() and plot() no longer print sf's s2 switch and
    planar-assumption messages, and dg_closest_res_to_area(show_info =
    TRUE) reports in one message.

  - The README and vignettes read grids and data through summary(),
    as.data.frame(), n_cells() and $ columns.

Breaking changes

  - ISEA cell IDs are 64-bit integers (bit64::integer64, a new import),
    exact at every resolution (#99). They were doubles, which hold whole
    numbers exactly only below 2^53: aperture 4 from resolution 25 and
    aperture 7 from resolution 18 returned IDs that decoded to other
    cells, and aperture 7 from resolution 22 IDs that did not decode.
    lonlat_to_cell(), get_parent(), get_children(), get_neighbors(),
    cell_to_sf(), hexify() and every other function returning ISEA cell
    IDs return integer64; functions taking them also accept whole
    numbers below 2^53 and digit strings, and refuse a double of 2^53 or
    more, which cannot name a cell exactly. H3 cell IDs stay character.
    n_cells() stays a double: it is a count, not an ID. unlist(),
    sapply(), ifelse() and for loops drop the integer64 class, leaving
    raw bits that read as numbers between 0 and 1: combine the lists
    get_neighbors() and get_children() return with do.call(c, x). A
    value between 0 and 1 passed as a cell ID is refused with that hint.

  - A grid's resolution is capped where its cell count passes 2^63 - 1,
    the largest integer64: on the icosahedron aperture 3 keeps
    resolutions 0-30, aperture 4 stops at 29 and aperture 7 at 21 (on
    the octahedron, aperture 4 reaches 30). hex_grid() names the finest
    resolution when asked for a finer one, and an area_km2 finer than
    that clamps to it. Aperture 7's Z7 index strings, previously limited
    to resolution 20, follow the grid to 21.

  - plot_globe() is removed. plot(<grid>) draws a grid in 3D, on the
    sphere (surface = "sphere") or on the flat faces of the solid the
    grid is built on (surface = "solid"), with land fill and country
    outlines from hexify_world or any sf polygons (land =), a center to
    look down on, and the solid's face edges. It takes the grid object,
    so any aperture, mixed sequence, resolution, solid or body reaches
    it; an H3 grid is drawn on the sphere. globe_centers names the views
    as before.

  - A grid built with one aperture per level stores it comma-separated:
    hex_grid(resolution = 4, aperture = c(4, 4, 7, 3))@aperture is
    "4,4,7,3", no longer "4/4/7/3"; aperture = "4,4,7,3" is accepted and
    "/" is kept for the two-aperture families such as "4/3". A two-level
    sequence c(4, 7) was stored as "4/7", the family name, so its
    parents were cells of the family's resolution-1 grid (aperture 7)
    rather than of its own first level (aperture 4). Cell IDs are
    unchanged. get_children() on a per-level grid now says that the
    spelling names no finer level.

  - The aperture-7 Z7 index on the icosahedron is now IGEO7's, string
    for string the one DGGRID writes: every cell at resolutions 0 to 7
    agrees with DGGRID, on ISEA and FULLER and under a given orientation
    (paper/bench/bench_dggrid_z7.R). Previously about two cells in three
    differed: a cell whose ancestry leaves its quad was written as quad
    + 12 * seed instead of the base cell it descends from, and the
    digits were not turned into a polar base cell's frame or past the
    direction each pentagon lacks. DGGRID's encoder is bijective: the
    two cells #53 reported as sharing a string get distinct strings from
    it, so that report came from hexify's own earlier port, not from
    DGGRID. The axis names of digits 3 to 6 were also swapped in hexify,
    which turned digit rotations the wrong way. A pentagon has six
    children, so hexify_get_children() returns six strings under one,
    and hexify_z7_canonical() sends a string in the deleted subsequence
    to the cell's own index. Cell IDs are unchanged. Octahedral grids
    keep the quad + 6 * seed leading field, since IGEO7 is defined on
    the icosahedron (#86).

  - hexify_cell_id_to_quad_ij() is removed. hexify_cell_to_quad_ij()
    takes the same arguments and returns the same quad, i, j data frame.

  - The inverse of Snyder's projection is now in closed form
    (Recht 2021), so hexify_set_precision(), hexify_get_precision(),
    hexify_set_verbose() and hexify_projection_stats() are removed, and
    hexify_inverse() no longer takes tol or max_iters. The inverse
    agrees with the previous Newton iteration to within 1e-13 radians on
    the icosahedron, octahedron and tetrahedron, returns a
    forward-projected point to within a few times 1e-15 radians, and is
    about twice as fast (#84).

New features

  - hex_grid(projection = "ivea") builds a grid on van Leeuwen and
    Strebe's (2006) vertex-oriented great-circle projection, the third
    face projection beside Snyder's and Fuller's: DGGAL's IVEA3H and
    IVEA7H, and any aperture, aperture sequence, orientation, the
    octahedron, and hex_globe(). It is equal-area, so cell_area() gives
    every hexagon of a resolution the same area, and its inverse is in
    closed form. With DGGAL's orientation (orientation =
    c(11.20, 58.282525588538995, 0)) every zone of IVEA3H at levels 1
    to 8 and of IVEA7H at levels 1 to 5 is a hexify cell, centres and
    corners within a micrometre (paper/bench/bench_dggal_agreement.R).
    hexify_forward() and hexify_inverse() take it on the tetrahedron
    too. DGGRID has no IVEA, so as_dggrid() refuses such a grid (#92).

  - projection_distortion() gives Tissot's indicatrix of a grid's face
    projection at any point: the scale factors, the maximum angular
    deformation, the areal scale and the direction of the longer axis.
    The derivative is exact, from forward-mode automatic differentiation
    of the projection code. Snyder's projection reproduces his Table 1
    (17.27 degrees, scale factors 1.163 and 0.860) and keeps areas
    to 1e-14; Fuller's areal scale averages 1. plot(<grid>, distortion =
    "angular" / "areal") colours the sphere, the solid or a net by it,
    and tissot = draws the ellipses on the solid and the net (#88).

  - Densified cell boundaries, the cell paths plot() draws and the
    globe's face and land meshes put a vertex where an edge crosses an
    arc from a face centre to a corner, where Snyder's projection bends,
    and densified boundaries also where they cross a face edge (#88).

  - plot(<grid>, parents = ) outlines the cells one or more resolutions
    up over the grid's cells; on a net, area_legend = TRUE draws one
    cell of each resolution at the map's scale with its area, and tabs =
    TRUE adds glue tabs along the seams so a printed net folds into the
    solid (#90).

  - hex_rays() draws Carr's ray glyphs, whose angle shows a value, with
    confidence arcs and a second ray for a second variable, and
    hex_triangles() cuts cells into one triangle per wall carrying the
    change towards that neighbour (#90).

  - net_cells() cuts cells into their parts on the pieces of a net
    layout, with each part's map area and which parts the map shows
    joined, so the cells a seam cuts can be counted; on Snyder's
    projection the parts of a cell add up to its area (#90).

  - hex_grid(polyhedron = "octahedron") builds an ISEA-family grid on
    the octahedron: Snyder's equal-area projection with g = 54.73561032
    deg, G = 45 deg (Snyder 1992), four diamonds, and six square vertex
    cells, each two thirds of a hexagon's area. Apertures 3, 4, 7 and
    mixed sequences, the hierarchy, indices, neighbours, cell_area(),
    plot() and hex_globe() work as on the icosahedron. The solid is a
    descriptor (vertices, faces, Snyder's constants) from which the face
    pairing, the quad adjacency and the vertex cells are derived, so the
    icosahedron's grids are unchanged bit for bit. Fuller's projection
    stays icosahedron-only (#94).

  - hexify_forward(), hexify_inverse(), hexify_which_face(),
    hexify_face_centers() and hexify_lonlat_to_plane() take polyhedron =
    "octahedron" or "tetrahedron". The tetrahedron carries the
    projection only, and hex_grid(polyhedron = "tetrahedron") says why:
    the cell numbering pairs faces into diamonds that leave exactly two
    vertices as single-cell poles, a vertex where three faces meet has
    to be one of them, and all four vertices of the tetrahedron are such
    vertices.

  - cell_to_sf(shape = , depth = ) draws ISEA cells as Gosper islands
    ("gosper") or as the outline of their descendants ("descendants");
    plot(), hexify_heatmap() and st_as_sf() on gridded data take the
    same arguments. A Gosper island replaces each cell edge depth times
    by three segments 1/sqrt(7) as long, turned by atan(sqrt(3)/5), on
    the face plane of the projection, so on an equal-area grid each
    island has its cell's area and the islands of a grid tile the
    sphere. The descendant outline of an aperture-7 cell is the boundary
    of its descendants depth resolutions down, which nests across
    resolutions; the child lattice of ISEA7H turns one way at one
    resolution and back at the next, so this outline stays close to the
    hexagon (#82).

  - get_children() on ISEA grids looks children up by match() rather
    than by name, so its time grows linearly with the number of cells:
    all 492 cells of an aperture-7 resolution-2 grid, three levels down,
    go from 123 s to 0.5 s.

  - hex_grid(projection = "fuller") builds an ISEA-family grid on
    Fuller's projection instead of Snyder's equal-area one: DGGRID's
    FULLER3H, FULLER4H, FULLER7H and FULLER43H, and any aperture
    sequence or orientation. The forward projection follows Gray (1995)
    and the inverse solves Gray's equation (39) in Crider's (2008) form
    in closed form, as a cubic solved by Viete's trigonometric method
    (#96). Cell IDs, the hierarchy and neighbours are those of the ISEA
    grid; centres and corners agree with DGGRID's FULLER output. Fuller
    cells are not equal-area, so area_km2 is the mean cell area and
    cell_area() returns each cell's own, from its solid angle.
    hexify_forward(), hexify_forward_to_face() and hexify_inverse() take
    projection, as_dggrid() and from_dggrid() carry FULLER,
    dggrid_is_compatible() accepts it, and hex_globe() finds Fuller
    cells per pixel as it does ISEA cells.

  - hex_globe(<grid>) draws a grid on an interactive globe, rendered on
    the graphics card through WebGPU (an htmlwidget; needs
    'htmlwidgets'). Drag turns the globe, Shift-drag tilts and turns the
    view, the wheel zooms, and a slider folds the icosahedron into the
    sphere. values fills the cells through a colour ramp (palette,
    limits, na_fill). It takes the camera and style arguments of
    plot(<grid>). The cells of an ISEA grid are found per pixel on the
    graphics card, with lonlat_to_cell()'s projection and numbering, so
    the page carries the grid's description and one value per cell
    rather than cell outlines: at aperture 3, resolution 10 (590,492
    cells), a globe with a value on every cell builds in 0.4 s, its page
    is 7 MB and a frame takes about 3 ms at 800 and at 1600 pixels. The
    shader's cells agree with lonlat_to_cell() to within 1e-5 radians.
    Borders keep their width at every zoom and fade out where cells
    shrink to a few pixels. The pointer shows the ID and value of the
    cell under it. H3 cells are drawn from their outlines. Without
    WebGPU the widget shows a notice.

  - hex_globe_png() saves a globe as a PNG, drawn with the widget's own
    WebGPU shader and pipelines and read back from the graphics card, so
    a machine without a display saves the globe rather than a blank
    page; pixels off the globe are transparent. renderer = "wgpu" (the
    default) draws through the companion package 'hexglobe' ('wgpu', the
    Rust WebGPU implementation) with no browser; renderer = "chrome"
    draws the widget in headless Chrome (needs 'chromote'). The two
    agree to within edge anti-aliasing (#107).

  - The article "Interactive Globe" on the package website shows
    hex_globe() live and lists the browsers that draw it. The globe
    keeps its colours in the site's dark mode.

  - Land is triangulated on the faces of the icosahedron in C++, so
    filled land folds with the grid; the land area of the mesh matches
    sf::st_area() of hexify_world.

  - Both surfaces read their cell boundaries from the same points: each
    cell edge is walked in the face plane, where it is straight, and cut
    where it crosses a face edge, so a cell on the icosahedron is the
    cell on the sphere folded flat.

  - plot(<grid>) takes a viewpoint. projection = "perspective" places a
    camera distance sphere radii from the centre, tilt swings it about
    the point at center, which it keeps looking at, until the horizon
    shows, and fov sets its field of view; by default the frame holds
    the whole visible sphere. rotation turns either projection about the
    line of sight. Filled shapes are clipped at a plane in front of the
    camera, so any tilt below 90 degrees draws.

  - hex_grid(orientation = ) places an ISEA grid's icosahedron anywhere
    on the sphere (#80): c(vert0_lon, vert0_lat, azimuth) as DGGRID's
    dggs_vert0_lon, dggs_vert0_lat and dggs_vert0_azimuth take it,
    "random" (repeatable with set.seed()), or "region" with region = a
    point or an sf object, which centres the grid as DGGRID's
    REGION_CENTER does (on the midpoint of an icosahedron edge), or
    "face" with region =, which puts the region's centre on the centre
    of a face. The grid stores it in a new orientation slot, and every
    function taking the grid reads it, the hex_globe() shader included;
    grids saved before the slot existed read the standard orientation.
    Cell IDs, the hierarchy and neighbours are the same under every
    orientation. Under three given orientations and three region
    placements, 50,000 random points per grid fall in the same cells as
    in DGGRID for apertures 3, 4, 7, ISEA43H and a mixed sequence, with
    cell centres within 2 mm. H3 grids keep H3's own orientation.

  - as_dggrid(), from_dggrid() and dggrid_is_compatible() carry the
    orientation (pole_lon_deg, pole_lat_deg, azimuth_deg) instead of
    warning about or rejecting a non-standard one.

  - hexify_build_icosa() sets the orientation of the functions that take
    no grid, and no longer reaches grids; hex_grid() and hexify_grid()
    no longer reset it.

  - hex_grid(orientation = "dymaxion") places the icosahedron in
    Fuller's Dymaxion orientation (#89; Sahr et al. 2003): vertex 0
    at 5.2454W, 2.3009N, azimuth 7.46658, with all twelve pentagons in
    the ocean. With projection = "fuller" the grid lies on Fuller's
    Dymaxion map.

  - New net_layout() lays out the faces of a grid's solid as a flat map,
    and plot(<grid>, surface = "net", layout = ) draws on it (#83). A
    layout cuts faces into pieces and places each by a rotation and a
    translation, so a piece can appear more than once. "plane" is
    DGGRID's PLANE layout, as before; "gosper" is Van de Sande's Gosper
    World on the octahedron, four regular hexagons each of one face and
    a third of its three neighbours, equal-area on Snyder's projection;
    "gosper_flower" surrounds one hexagon by the six that border it;
    "gosper_land" joins the four hexagons along the sides that cross the
    most land, as in Van de Sande's Fig. 1; "rhombic" is DGGAL's 5 x 6
    rhombic space on the icosahedron, each face sheared onto half a unit
    square; "land" cuts the solid along the edges that cross the least
    land, a Dymaxion-style net with orientation = "dymaxion", projection
    = "fuller". net_project() places points on a layout. seams = TRUE
    draws the cuts of the net.

  - hex_grid(polyhedron = "octahedron", orientation = "gosper") puts the
    poles at midpoints of octahedron edges, as the Gosper World does,
    with all six vertices at least 619 km offshore.

  - plot(<grid>, graticule = ) draws meridians and parallels on the
    sphere, the solid and the net.

  - New cell_metrics() and wall_metrics() measure a grid's cells: area,
    perimeter and compactness (White et al. 1998) per cell, and per wall
    between adjacent cells its length, the distance between the two
    centres and the cell wall midpoint ratio (Gregory et al. 2008).
    Without cell_id they measure every cell of the grid. ISEA walls are
    followed on the sphere as the projection curves them, for Snyder's
    and Fuller's projections alike; H3 walls are read from H3's directed
    edges.

  - New hex_smooth() smooths cell values over neighbouring cells (#90):
    each step replaces a value by the weighted mean of the cell and its
    listed neighbours, the neighbour-matrix smoother of Carr et al.
    (1997). group keeps groups such as land and ocean apart; NA values
    and unlisted cells do not contribute.

  - Zone queries of OGC Topic 21 (#105). get_parent(overlapping = TRUE)
    returns every coarser cell a cell reaches into (Topic 21's parent()
    with inheritID false): three for an aperture-3 cell on a parent
    corner, two for an aperture-4 cell on a parent edge or an aperture-7
    cell off the parent's centre. get_siblings() returns the other
    children of a cell's parent, and get_neighbors(ring = TRUE) the
    hollow ring of cells exactly k hops away.

  - hex_grid() reads an aperture list shorter than the resolution as a
    recurring sequence, as Topic 21 reads its list of refinement ratios:
    aperture = c(4, 3) at resolution 5 refines by 4, 3, 4, 3, 4 (#105).

  - dggrs_definition() describes a grid in the DGGRS definition schema
    of OGC API - DGGS: solid, refinement ratios and strategy, zone
    shapes, orientation, zone identifiers and sub-zone order (#105).

  - cell_metrics() adds normalized_area, a cell's area over the grid's
    mean, and ipq, the isoperimetric quotient 4 pi A / p^2, the two
    measures of Kmoch et al. (2022) (#106).

  - The article "OGC Topic 21 Conformance" sets hexify against each
    requirement of Topic 21, with the Earth model and its area error on
    WGS84, the distance between cell centre and area centroid, and how
    often binning and taking the parent disagree with binning at the
    parent's resolution (#105, #106).

Bug fixes

  - grid_global() returns every cell of an ISEA grid. It sampled points
    and kept the cells they hit, which missed small polar cells from
    about 40,000 cells on: 2 of 40,962 cells at aperture 4
    resolution 6, 54 of 168,072 at aperture 7 resolution 5. It now lists
    the cell IDs 1 to n.

  - get_parent() on a mixed aperture grid finds the parent in exact
    lattice coordinates (#85). After an aperture-3 step a child centre
    sits on a corner of three parents, and after an aperture-4 step on
    an edge shared by two; the parent of such a child was decided by
    floating-point rounding of the projected centre, so a hexagonal
    parent received anywhere from 1 to 7 children. A fixed tie rule now
    gives every hexagonal parent inside a quad exactly as many children
    as the step's aperture. Children whose centre lies strictly inside a
    parent keep their parent. get_children() and cell_to_index() follow
    get_parent(), so index strings of mixed grids change for the tied
    children; regenerate stored ones. levels > 1 steps up one level at a
    time.

  - Cells in the last column of a quad at aperture 3, resolutions 25-30,
    were assigned to another quad: the quad edge dimension was computed
    in floating point and came out one too small. It is now exact.

  - cell_area() on an ISEA grid returns each cell's area: every hexagon
    of a resolution has S / (N - 2) of a body of area S split into N
    cells, and each of the 12 pentagons 5/6 of that. It returned the
    mean S / N for every cell. The cell_area_km2 column of a HexData
    object and of as.data.frame() reads cell_area(), so it carries the
    same areas, on the Fuller projection each cell's own.

  - hexify_world is simplified as a coverage, so neighbouring countries
    share their simplified borders exactly. It was simplified country by
    country, which left about 480 slivers of gap between neighbours;
    they showed as thin lines wherever land was filled without country
    outlines. Attributes are unchanged.

  - A cell at an icosahedral vertex now drops the corner that lies in
    the icosahedron's angular deficit, found from the faces rather than
    a fixed corner number. Under the lattice turn of a mixed sequence
    such as c(4, 4, 7, 3) the fixed number named the wrong corner, and
    those twelve pentagons were drawn with a corner read off the wrong
    face.

  - On a mixed aperture sequence with an odd number of aperture-7 levels
    ("4/7" at odd resolutions, c(4, 4, 7, 3), c(7, 3), ...), the cell
    lattice is turned by atan(sqrt(3)/5) and its cells straddle the quad
    edges, so a point near an edge can have its nearest centre in the
    next quad. lonlat_to_cell() clamped that centre back onto the quad
    and read it as an unrelated cell: 2,500 to 4,200 boundary points per
    grid went to the wrong cell, and grid_global() missed a cell of
    c(4, 4, 7, 3) and of c(4, 3, 7). The centre now moves to the quad
    that owns it through DGGRID's edge table, as pure aperture 7 already
    did.

  - At odd aperture-3 resolutions, a point on or just across an
    icosahedron face edge could quantize to a centre two rows outside
    its quad, which lonlat_to_cell() read as an unrelated cell (often a
    pentagon). Such centres now go through the same edge table. Points
    away from face edges keep their cells.

  - The Snyder projection constants are computed from their closed forms
    at full double precision (tan θ = 3 - sqrt(5), a face edge of
    sqrt(4π / (5 sqrt(3))), R' = edge / (sqrt(3) tan θ), vertex
    latitude atan(1/2)) instead of being carried to 8 to 10 digits, and
    the icosahedron's vertices are rotated into place on unit vectors,
    which places them to 4e-16 where they were off by 1e-8. Cell
    assignments move only for points within about 1e-8 rad (6 cm) of a
    cell boundary: 2 of 3.8 million random points across 19 grids.

  - Forward and inverse projection put an azimuth that lies exactly on a
    120° sector boundary, a line from a face centre through a vertex, in
    different sectors, so the inverse returned such a point on the
    mirrored side of the face. Both now read one half-open sector rule.
    A point on a face edge now gets one of the two cells beside it at
    every resolution.

                        Changes in version 0.8.4                        

Breaking changes

  - ISEA cell polygons follow the true cell edges. An edge is straight
    in the projection plane and curved in lon/lat, so cell_to_sf(),
    grid_rect(), grid_global(), hexify_cell_to_sf() and the plot methods
    now split each edge until every piece is a straight lon/lat chord to
    within 0.1% of its length, as DGGRID's densification does. Drawn
    with their six (or five) corners alone, cells were off by up to 4%
    of their area at aperture 3 resolution 8 and up to 19% at coarse
    resolutions; now within about 0.1%. A polygon now has six corners
    and the points between them, so a cell's vertex count is no longer
    six; the corners themselves are unchanged.

Bug fixes

  - At odd aperture-7 resolutions, lonlat_to_cell(), hexify() and the
    index functions gave a point near a cell edge to a neighbouring
    cell: 7 to 11% of random points over central Europe at resolutions 2
    to 7. A point was snapped to the finer substrate lattice and then to
    that point's parent, so a cell was the union of seven substrate
    hexagons rather than a hexagon. Points now go to the cell whose
    centre is nearest, the hexagon its polygon draws, which changes the
    cell ID of those points.

  - A cell straddling a quad edge was drawn with the corners beyond the
    edge read on its centre's face stretched across it, and an
    icosahedral-vertex cell with its folded edge drawn straight across
    the missing sector. Both are now drawn where lonlat_to_cell() places
    their boundary.

  - grid_rect() on ISEA grids missed cells that intersect the bounding
    box, mostly along its edges and occasionally inside it. It now seeds
    from a point sampling and grows through neighbouring cells that meet
    the box until none is added, so the result is every cell meeting the
    box, independent of the sampling density (#77). hexify_grid_rect()
    had its own copy of the sampling and now calls grid_rect().

                        Changes in version 0.8.3                        

Breaking changes

  - as_sf() is now sf::st_as_sf(). sf owns the verb, so hexify registers
    methods on it for HexData and HexGridInfo and re-exports it, and an
    sf pipeline reaches a hexify object without knowing hexify's own
    name for the conversion. st_as_sf() on a grid specification returns
    its global cell set. The geometry argument follows ..., as the
    generic's signature requires, so name it: st_as_sf(x, geometry =
    "polygon").

  - The index entry points that returned one value now return one per
    index: hexify_index_to_cell() and hexify_index_to_lonlat() a data
    frame with a row per index, hexify_get_children() a list with an
    element per index. Reading a single index out of them takes [[1]] or
    [1, ].

New features

  - summary() on a grid or on gridded data returns what printing it
    reports, as a list: grid type, aperture, resolution, area, diagonal,
    CRS, radius and cell count for a grid; row and column counts, column
    names, cell count, storage type and the grid's own summary for data.
    Printing an object prints this summary, so what it shows and what it
    returns are the same thing.

Bug fixes

  - get_children() returned cell IDs past the end of the child grid on
    apertures 3 and 4, and NAs on aperture 7, at the twelve icosahedron
    vertices, so get_parent() stopped on its own output there. A vertex
    sits at the corner of several quads and has an index spelling in
    each, and appending a digit to one spelling names cells the other
    quads hold: some digits named nothing, and the children spelled
    under another quad were never reached. Children are now read back
    from get_parent() over candidates drawn from both the index
    expansion and the ring of child cells around the parent, so they
    invert get_parent() exactly. Between them the parents of a
    resolution now claim every cell of the child grid once, and an
    aperture-7 pentagon has six children rather than five and two NAs.

  - hex_compact() and hex_uncompact() refused every ISEA grid but
    aperture 7, and did their arithmetic on the index strings. Both read
    cells now, so all three pure apertures compact: cells are grouped by
    get_parent(), and a group holding as many distinct cells as the
    parent has children replaces them, which reads the six children of a
    cell at an icosahedron vertex rather than assuming seven.
    Uncompaction expands through get_children() for the same reason, so
    the children spelled under a neighbouring quad are reached. An index
    string that names no cell of the grid, which appending a digit to a
    vertex cell writes, is refused by name rather than reported as a
    cell number the caller never wrote. A full grid compacts to the
    twelve resolution-0 cells at every aperture.

  - get_parent() on an aperture-7 grid at resolution 1 returned cell IDs
    past the end of the resolution-0 grid -- up to 83, where
    resolution 0 holds 12 -- and only 42 distinct ones, so
    get_children() at resolution 0 came back in threes rather than
    sixes. A Z7 index spells its leading field as quad + 12 * seed, the
    seed naming the point the hierarchy walk arrives at, and a
    two-character index was read as a bare quad number. It is decoded
    now: the seed says which of the base cells meeting at the quad's
    corner the index reaches, through the same adjacency the encoder
    walks. Every resolution-0 cell sits on an icosahedron vertex and now
    has the six children that make up resolution 1's 72, at every
    aperture the parents of a resolution claim each cell of the finer
    one exactly once, and a full grid compacts to the twelve
    resolution-0 cells at apertures 3, 4 and 7 rather than stopping
    at 52.

  - hexify_lonlat_to_index() documented itself as vectorised over lon
    and lat and took one point, and nothing that consumes an index
    string accepted the vector cell_to_index() returns:
    hexify_index_to_cell(), hexify_index_to_lonlat(),
    hexify_get_parent(), hexify_get_children(), hexify_get_resolution(),
    hexify_compare_indices() and hexify_z7_canonical() each stopped on
    Rcpp's own scalar-coercion message, which named no hexify function
    and no argument. Every index entry point takes vectors now and loops
    in C++, so a caller pays one call rather than one per element, and a
    missing coordinate or index carries through as a missing result.
    Arguments read in step whose lengths differ are named, with the
    function they were passed to.

  - hex_distance() over-reported the hop count between cells of one
    quad, by up to three times, and at aperture 7 also under-reported it
    by half. It read the difference of the two cells straight off the
    substrate the cell IDs are packed on, which is the cell lattice only
    some of the time: at aperture 3's odd resolutions one substrate
    point in three is a cell and the six neighbours stand sqrt(3)
    substrate units apart, so a step was counted as three. The
    handedness of the axial formula was wrong as well. The difference is
    now divided by the lattice generator first, which reads it in cell
    steps, and the six neighbours are the six units there. Every
    adjacent pair of cells is one hop apart at apertures 3, 4 and 7, and
    the distances agree with breadth-first search over get_neighbors().

  - get_neighbors() and hex_distance() read a grid's aperture as a
    single number, which is NA for a sequence spelling, so every grid
    built from one -- "4/3", "4/7" or a per-level vector -- stopped on a
    coerced-aperture message while every other operation on it worked.
    One walk now serves both: it takes what a quad edge measures, which
    substrate lattice the cells sit on, and how a quad coordinate maps
    to and from the plane, and a pure aperture and a sequence differ
    only in those. A mixed grid has the twelve cells with five
    neighbours and six everywhere else that a pure one has, at every
    sequence and resolution tested, and its distances agree with
    breadth-first search.

  - grid_clip(), hex_zonal() and hex_extract() stopped on a grid built
    for another body, with sf's own st_crs(x) == st_crs(y) is not TRUE,
    which names neither the function nor the boundary nor the radius
    that separates them. A boundary and a grid on two bodies are now
    reconciled in one place: two CRSs on the same body reproject as
    before, and where the bodies differ, longitude and latitude name the
    same region on either sphere, so a lon/lat boundary is read in the
    grid's own CRS and the caller is told which two CRSs met. A
    projected CRS carries lengths that do not carry over, and is refused
    with both named. hexify() read sf coordinates the same way,
    transforming them to WGS84 whatever body the grid was on, and now
    reads them on the grid's own.

  - The grid_clip() and plot_grid() examples asked for France and got a
    third of the globe: the world map's France carries the overseas
    departements, and both functions cover the bounding box of what they
    are given, which for all of France runs from -62 to 56 degrees of
    longitude. That built 36707 cells to draw metropolitan France. The
    examples crop to the metropolitan extent, which is what they meant,
    and together they now take under a second rather than fifty.

  - as_dggrid() on a grid built for another body returned a dggs that
    read as an Earth grid, and said nothing. A dggs has no field for the
    radius, so the conversion warns and names the radius being dropped.
    from_dggrid() is the same boundary in reverse and takes a radius_km
    argument, defaulting to Earth, so a grid on another body
    round-trips.

  - hexify_heatmap() handled an RColorBrewer palette only between three
    levels and the palette's own length. A breaks argument with ten or
    more bins passed fewer colours than the scale needed and stopped in
    ggplot2, naming ggplot2 rather than the palette; fewer than three
    bins let RColorBrewer's own warning through. The palette is now
    taken at exactly the number of levels asked for, by interpolating
    over its full range past its length, and it matches
    RColorBrewer::brewer.pal() colour for colour within it.

  - A colors name in neither the viridis nor the RColorBrewer family was
    passed to viridisLite, which drew the default palette and warned
    about its own argument. It is an error now, naming both families.

  - n_cells() had no HexGridInfo method, so asking a grid how many cells
    it holds stopped on dispatch while printing the grid reported
    exactly that number. Both read one function now. On a HexData the
    count is still the number of distinct cells its rows fall in.

  - tibble::as_tibble() on a HexData reached tibble's default method and
    stopped there, unable to coerce an S4 object. as_tibble.HexData()
    was exported rather than registered, so dispatch never found it. It
    is registered on tibble's generic now, and tibble stays in Suggests.

  - cell_to_sf() returned an empty polygon for every cell whose boundary
    lay within two degrees of a pole, which from resolution 4 at
    aperture 7 and resolution 5 at aperture 4 is each cell nearest the
    poles. Corners inside that band were snapped onto the pole, so once
    a cell was narrower than the band all of its corners landed on one
    point. A pole falls inside a cell or on a cell edge, never on a
    corner, and corners now keep the position the inverse projection
    gives them.

  - grid_global() emitted GDAL errors and returned an invalid geometry
    for the cells that cover a pole. The ring of such a cell winds once
    around the globe, which no lon/lat ring closes, and
    sf::st_wrap_dateline() joined the two halves it cut along a line of
    constant latitude, across the cell's own edges. The ring is now cut
    at the antimeridian and carried up to the pole, the way a polar cap
    is drawn in lon/lat.

  - A cell split at the antimeridian keeps its shape. The cut ends were
    joined along a line of constant latitude, which is not where the
    cell's edge runs and moved as much as a tenth of the area of a cell
    at the seam. The ring now carries a corner on its own edge where it
    meets +/-180, and a cell covers the same area whether or not it is
    split.

  - Cell rings cross the antimeridian by carrying their longitudes on
    continuously rather than by shifting the negative ones, which placed
    a ring spanning exactly half the globe on both sides of the map at
    once. Only cells reaching past +/-180 go to sf::st_wrap_dateline(),
    which cuts any segment half a turn wide and so also split the edge a
    cell runs over a pole.

                 Changes in version 0.8.2 (2026-08-22)                  

Bug fixes

  - lonlat_to_cell() on a mixed-aperture grid names a cell for a point
    that sits on a quad corner, which is where the twelve icosahedron
    vertices put the poles. The rotate/requantize chain can leave the
    quad's coordinate range by a tie-breaking unit there, and the cell
    index reads the pair as an unsigned offset from the quad origin, so
    such a point came back out of range and cell_to_index() stopped on
    it.

  - The default ISEA orientation carries atan(phi) at full double
    precision, with phi = (1 + sqrt(5)) / 2, placing vertex 0 at
    latitude 58.282525588538995 degrees. The constant was stored rounded
    at the eighth decimal, 1.5e-09 degrees from the analytic value,
    which is 0.16 mm on Earth and reaches the assignment of a point that
    close to a cell boundary.

  - cell_to_sf() on a mixed-aperture grid reads its cells with the
    aperture sequence the grid was built from. The polygon path took a
    single aperture, which a mixed sequence does not have, and decoded
    every mixed grid as aperture 3: cell IDs past that grid's range
    stopped the call and the rest named different cells. is_pentagon()
    read the same collapsed aperture and is on the sequence path as
    well.

  - Aperture-7 hexagons carry the orientation of their own lattice,
    unrotated at even resolutions and turned 19.1 degrees at odd ones.
    The corners were placed 30 degrees off the lattice at every
    resolution, which aimed them at the neighbouring cell centres rather
    than the points between them. Corner positions now come from the
    grid form, so every aperture reads its rotation and scale from one
    place.

  - A cell at an icosahedral vertex drops the corner that falls in the
    icosahedron's angular deficit on a rotated lattice too. The dropped
    corner was fixed to the unrotated layout, so aperture-7 pentagons
    and aperture-3 pentagons at odd resolutions kept a corner no
    neighbour reaches and dropped one they share.

  - hexify_cell_to_sf() returns the same boundary whichever way
    return_sf is set. The data frame path was a second implementation,
    without the pentagon handling, the fallback for a corner outside its
    quad, or the extension of polar cells to the pole, so it stopped on
    every pentagon and drew edge cells differently. Both paths read one
    boundary routine now, and the data frame gives a pentagon six rows
    rather than seven.

  - get_parent() and get_children() honour levels on pure ISEA grids,
    where they moved one level whatever was asked for.

  - get_children(get_parent(x)) contains x. get_parent_index() stripped
    two levels off a Z3 or aperture-3 z-order index whenever its digit
    count was even, and one instead of two off an aperture-7 z-order
    index; get_children_indices() rebuilt aperture-3 and aperture-4
    children from a coordinate box rather than appending the child
    digit, which is not the inverse of stripping one. Over every cell ID
    the round trip goes from 4.3 per cent to 100 per cent for aperture 3
    at resolution 2, and from an error to 98.2 per cent for aperture 7,
    where the cells below a pentagon remain: a pentagon has six
    aperture-7 children, so a Z7 index does not name seven distinct
    descendants there.

  - quad_ij_to_cell() re-expresses a coordinate that has stepped outside
    its quad in the quad that owns it, and returns NA where the
    icosahedron folds at a vertex and no quad does. Such a pair was
    packed as it stood, giving cell IDs past the end of the grid.

  - The compiled entry points all carry the cpp_ prefix. Five unprefixed
    duplicates at the foot of rcpp_index.cpp had no callers, and
    compileAttributes() wrote each one into R/RcppExports.R, where
    cell_to_index() collided with the exported cell_to_index(cell_id,
    grid), and which definition survived came down to the order R
    sources R/.

Documentation

  - The README and the theory vignette say ISEA takes apertures 3, 4
    and 7 in any sequence, which has held since 0.8.0. They named 4/3 as
    the one mixed sequence ISEA reads.

                        Changes in version 0.8.1                        

New features

  - crs takes any coordinate reference system sf reads: an EPSG code as
    before, or a 'PROJ' or 'WKT' string. A grid left to its default is
    read on its own body, so a grid built with radius_km carries a
    longlat CRS on the sphere of that radius and an Earth grid keeps
    EPSG:4326. EPSG codes name Earth reference systems, so a grid on
    another body had no code to name it and was reported in 'WGS84'.

  - Every function that hands out coordinates reads the grid's CRS
    through grid_crs(), the way kilometres go through grid_radius_km().
    cell_to_sf(), as_sf(), hex_summarize(geometry = TRUE) and
    hex_extract() all return a grid's own coordinates rather than
    assuming Earth.

Bug fixes

  - plot_globe() returns in about a second on the grids its own examples
    use, where it took eleven minutes. Cells on the hemisphere edge come
    out of the orthographic transform as rings of two points, which GEOS
    rejects for the whole set, so every call fell through to a per-cell
    repair loop that rewrote the whole table once per cell. Those cells
    are now dropped before the repair, which lets the batch call
    through, and the loop that remains as a fallback assembles its
    geometries once. The cells the function returns are unchanged.

Documentation

  - hexify_heatmap() documents the basemap formats it takes, including
    the rasters it already accepted, and names sf::st_read() and
    terra::rast() as the way a file becomes one.

  - The quickstart says what an sf object is where it first uses one,
    and the bodies section of vignette("workflows") covers coordinates
    on another body.

  - Reported by Christian Carey.

                        Changes in version 0.8.0                        

Breaking changes

  - Aperture-7 cell IDs and Z7 index strings change. The IDs now span
    [0, 7^res) within a quad and the index carries the hierarchy seed in
    its leading field, both of which the Bug fixes below explain.
    Aperture-7 IDs and indices stored from an earlier version do not
    name the same cells and need regenerating from the coordinates.
    Aperture-3 and aperture-4 cell IDs are unchanged.

  - hexify_assign() assigns about a third of points to a different cell,
    at every aperture and effective resolution, because the quantizer
    built its cube triple from the wrong pair of axes. The cell it used
    to name sits 0.68 to 0.87 cell spacings from the point, further than
    the circumradius, so the point fell outside it. Its id is now the Z3
    index string and its face the quad, and the match_dggrid_parity
    argument is gone; it was never wired to an effect.

  - cell_to_lonlat() returns the centres of the two vertex-quad
    pentagons at (11.25, 58.28) and (-168.75, -58.28), where those cells
    are under the ISEA default orientation, rather than at the
    geographic poles.

New features

  - Grids cover any body, not just Earth (#56, requested by Christian
    Carey). hex_grid(radius_km = ) takes a radius in kilometres or a
    body name -- "mars", "moon", "titan", "europa" and thirteen others,
    at the IAU mean radii (Archinal et al. 2018) tabulated by JPL Solar
    System Dynamics. The grid object carries the radius, so everything
    reporting kilometres follows it: dgearthstat(), cell_area(),
    hexify_compare_resolutions(), the sf exports, and the resolution a
    target area_km2 picks. Cell geometry is angular and unchanged -- a
    coordinate lands in the same cell on every body -- and Earth remains
    the default, sized against the WGS84 ellipsoid area as before.

  - Both backends take a radius. H3 reports a cell's area as its solid
    angle times Earth's radius squared, so another radius scales those
    areas by the square of the radius ratio, exactly; cell_area(),
    dgearthstat(), hexify_compare_resolutions(type = "h3"), the
    resolution an area_km2 picks and h3_crosswalk() all follow. An H3
    cell ID names a position in H3's topology, which Uber's H3 reads on
    Earth, so the IDs of a grid on another body are that topology on
    that body and are not interchangeable with Earth H3 data; hex_grid()
    says so once per session and h3_crosswalk() needs both grids on the
    same body.

  - hex_grid() takes any aperture sequence, so mixed grids beyond
    ISEA43H are reachable from R (#57). A family name splits the levels
    in two, as "4/3" already did -- "4/7", "7/4", "3/7" -- and a vector
    names one aperture per resolution level, aperture = c(4, 4, 7, 3).
    Cell IDs, centres, neighbours, the geometric hierarchy and the
    resolution-for-area inversion all follow the sequence. The cell
    count of a sequence is 10 * prod(apertures) + 2.

  - Cell IDs pack onto the substrate sublattice of any grid form. A
    sequence with an odd number of aperture-7 levels leaves the cells on
    a lattice of norm 7 or 21 rather than the norm-1 or norm-3 lattices
    pure apertures give; all four are now the single congruence j = c *
    i (mod N), which the aligned and Class II packings turn out to be
    the N = 1 and N = 3 cases of.

  - Mixed aperture sequences accept aperture 7 alongside 3 and 4, in any
    order (#55, requested by Christian Carey). Scale and lattice
    orientation now come from one model shared by the pure-aperture and
    mixed-sequence code: refining by aperture a multiplies the lattice
    generator by an Eisenstein integer of norm a (1 + w for 3, 2 for 4,
    2 + w for 7), so orientation is the product of the steps taken
    rather than a per-level Class I/II flag. The C++ entry points are
    now hex_quantize_mixed(), hex_center_mixed() and
    hex_corners_mixed(), replacing the _ap34 names, which no longer
    describe what they accept.

Bug fixes

  - Aperture-7 cells now round-trip through their Z7 index (#53). A cell
    was encoded by walking it up the aperture-7 hierarchy to
    resolution 0, and decoded by walking the digits back down from the
    origin, but the walk does not arrive at the origin for every cell: a
    quad is a rhombus while the aperture-7 parents are hexagons, so the
    quad boundary cuts through the parents of the cells along it, and
    those arrive at one of the six neighbours of the origin instead. Two
    thirds of all cells do, and the encoder discarded where it landed,
    so cell_to_index() followed by cell_to_quad_ij() returned the
    starting cell for only 33 to 54 per cent of them. The index now
    carries the arrival point in its leading field as quad + 12 * seed,
    with seed one of the seven unit digits, and decoding seeds the walk
    from it. Every cell round-trips at resolutions 1 to 9, distinct
    cells keep distinct indices, and the index length is unchanged at 2
    + resolution. A cell whose ancestry stays inside its quad has seed
    = 0 and keeps the plain two-digit quad, so the two cells DGGRID's
    own encoder merges onto 0045310 now read 2752310 and 2545310. Read
    the quad from an index with BB %% 12 or hexify_index_to_cell(). A
    coordinate that lies outside the quad it is given is now rejected
    rather than encoded to a string that names a different cell.

  - Aperture-7 cell IDs ran outside the documented [1, 10 * 7^res + 2]
    range and validate_cell_id() rejected them: a uniform sample at
    resolution 2 reached 2647 against a nominal count of 492, and at
    resolutions 1 and 2 the IDs also overran the padded space the
    encoder itself allotted, so cell_to_quad_ij() errored on ids the
    assignment had just produced. Aperture 7 stored each cell in a
    square bounding box sized by a fitted constant, roughly three times
    the cell count, because the surrogate lattice sits at 19.1 degrees
    to the quad frame. Cells now index by walking the quad's own
    substrate box, where the centres are the sublattice 2u + v = 0
    (mod 7) and a row holds one seventh of the box, so the IDs of a quad
    span [0, 7^res) with no gaps and the cell count is 10 * 7^res + 2
    for every aperture.

  - Aperture 7 gave the same cell two addresses at odd resolutions, once
    from each quad an edge cell straddles: a uniform sample at
    resolution 1 produced 122 distinct (quad, i, j) for a grid of 72
    cells. One cell there covers seven substrate points, and the quad
    was decided from the sampled point rather than the cell, so a cell
    whose points fall on both sides of an edge belonged to both. The
    cell centre now picks the quad, which puts each cell in exactly one:
    every ID in [1, 10 * 7^res + 2] names one distinct cell and
    round-trips through cell_to_quad_ij().

  - ISEA neighbour offsets stepped along the wrong lattice directions.
    The aligned lattice writes (i, j) as x = i - j/2, so (-1, 1) and (1,
    -1) stand sqrt(3) units away rather than one, and aperture 3's odd
    resolutions carry cells on the 30 degree lattice, where neighbours
    are sqrt(3) substrate units apart. Aperture 3 at resolution 5
    returned as few as two neighbours for a cell and one ID past the end
    of the grid; apertures 3 and 4 reported second-ring cells, at 1.6
    to 2.4 times the cell spacing, as neighbours of the 12 pentagons. A
    neighbour that under-runs its quad's frame has no image in it and
    used to be dropped; it now steps to the owning quad through DGGRID's
    edge table. Both poles are read from an adjacent quad's corner,
    where the offsets reach the five surrounding cells, instead of from
    their own single-cell frame, which the south pole overran into IDs
    past the end of the grid and the north pole left empty.
    Resolution 0, where all 12 cells are pentagons and each quad holds
    one cell, reads the icosahedron's vertex graph. Every aperture at
    resolutions 0-4 now reports exactly 12 pentagons, hexagons
    elsewhere, a symmetric adjacency, and no neighbour centre further
    than 1.12 cell spacings.

  - cell_to_lonlat() reported the two vertex-quad pentagons (quads 0
    and 11) at the geographic poles (#58). Those cells sit at
    icosahedron vertex 0 and its antipode, which are the poles only
    under a pole-aligned orientation; under the ISEA default they are at
    (11.25, 58.28) and (-168.75, -58.28), about 3500 km away. Both now
    come from folding the cell through the quad frame, as the polygon
    and mixed-sequence paths already did, so a point assigned to one
    gets its own cell's centre back. index_to_cell()'s inverse carried
    the same hardcoded pair and is fixed with it.

  - hexify_assign() returned centres thousands of kilometres outside the
    assigned cell for a share of points -- 113 of 400 uniformly sampled
    points further than two centre spacings at effective resolution 2
    (#58). It was the one path that quantized against raw triangle-local
    coordinates instead of folding into the non-negative quad frame, so
    it needed its own Z3 digit scheme (magnitude digits plus two
    trailing sign digits) whose round trip through z3::encode()/decode()
    did not preserve (i, j). It now runs on the same pipeline as
    hexify(): lonlat_to_cell() for the cell, cell_to_lonlat() for the
    centre, cell_to_index() for the ID. Every sampled point is now
    within one circumradius of its cell centre at effective
    resolutions 1-8. The reported id is the Z3 index string, face is the
    quad (0-11), and the match_dggrid_parity argument is gone -- it was
    never wired to an effect, and the shared pipeline is the
    DGGRID-verified one. cpp_hex_index_z3_*() are removed with the
    scheme they served.

  - Quantization built the cube triple (i, j, -i-j) from the grid
    coordinates. The i and j axes point at 0 and 120 degrees, so they
    are two of the three cube axes rather than the adjacent pair
    round-and-fix expects, and the step that corrects the coordinate
    with the largest rounding error corrected the wrong one: against
    brute-force nearest-centre over 3000 points, the quantizer returned
    a cell other than the nearest for 1026 of them. The triple is (i -
    j, j, -i), which now matches brute-force nearest-centre on every
    sampled point. This reached hex_quantize_ap3/4/7(), the
    mixed-sequence quantizer and hexify_assign(), whose cell assignment
    changes for about a third of points; the quantization is scale-free,
    so the share is the same at every effective resolution. Cell IDs and
    hierarchical indices are unaffected -- they quantize through
    quad_xy_to_ij(), which uses the exact region-classifying quantizer.

  - The aperture-7 grids returned by the hex_*_ap7() helpers were not
    centre-nested: each level applied a fixed kAp7RotDeg offset plus
    a 30-degree alternation, which is not an aperture-7 refinement (the
    norm-7 Eisenstein generators sit at +/- kAp7RotDeg, not 30 degrees),
    so a cell centre was about 0.85 of a child cell spacing away from
    the nearest centre one level down. Levels now alternate the two
    norm-7 generators, putting even resolutions at 0 degrees and odd
    resolutions at kAp7RotDeg on a sqrt(7) substrate -- the same
    convention as the exact-integer aperture-7 route used for cell IDs,
    whose divisor is 7^ceil(res/2).

  - Mixed sequences computed orientation from the count of aperture-3
    levels including the base level, and treated any sequence ending in
    aperture 4 as unrotated. A sequence such as c(4, 4, 3) was
    therefore 30 degrees away from the grid it describes, and c(4, 3, 4)
    was not nested in c(4, 3).

  - The aperture-7 inverse in quad_ij_to_xy() scaled by a
    sqrt(7)/sqrt(21) substrate while the forward quantization used the
    exact 7^ceil(res/2) one. Callers that reached it --
    hexify_index_to_lonlat(), hexify_quad_ij_to_xy() and
    hexify_quad_ij_to_icosa_tri() -- placed aperture-7 cells in the
    wrong location, or outside the quad entirely, where the conversion
    then failed. DGGRID's DgHexGrid2DS toggles Class III on every
    aperture-7 level, so even resolutions are an unrotated Class I grid
    and odd ones carry one aperture-7 level; the inverse now takes the
    same exact-integer route as the forward. The float-rotation Class
    III helpers that encoded the old substrate (quantize_class3i(),
    quantize_class3ii(), substrate_to_surrogate_ap7(),
    surrogate_to_substrate_ap7()) were unused and have been removed.

  - hexify_index_to_lonlat() returned the icosahedron vertex instead of
    the pole for cells in the polar pentagon quads, at every aperture.
    It now answers those directly, as cell_to_lonlat() does.

                        Changes in version 0.7.5                        

Bug fixes

  - kAp7RotDeg was mislabeled as atan(sqrt(3/7)) (~33.2 deg) and its
    numeric literal drifted from the true value starting at the 12th
    significant digit, present since the initial release. The correct
    closed form is atan(sqrt(3)/5), now cross-checked against DGGRID's
    M_AP7_ROT_DEGS and updated to full double precision. A duplicate
    literal in coordinate_transforms.cpp was removed in favor of
    deriving from the same constant (reported by Christian Carey).

                        Changes in version 0.7.4                        

Documentation

  - Documented the bijective aperture-7 Z7 format, exact round-trip
    guarantee, index structure, and the limited pentagon-region
    difference from DGGRID's non-injective raw Z7 encoding.

New features

  - Hierarchical navigation now works for mixed aperture "4/3" (ISEA43H)
    grids: get_parent(), get_children(), and cell_to_index() no longer
    error on these grids (#31). Mixed 4/3 grids have no DGGRID-standard
    hierarchical index, so hexify defines the hierarchy geometrically --
    a cell's parent is the coarser cell whose lattice point contains the
    cell's centre. This is the correct relationship for how these grids
    quantise (a single scaled quantisation per resolution, not a
    step-composed subdivision), and it round-trips cell ->
    cell_to_index() -> cell exactly, including the seam and
    icosahedron-vertex cells that a purely local walk would miss.

                        Changes in version 0.7.3                        

Bug fixes, package hygiene, and test coverage

Fixes

  - Fixed hexify_lonlat_to_index()/hexify_index_to_lonlat() for
    aperture 3: quantization skipped the quad-frame fold that
    aperture 4/7 already had, producing indices inconsistent with
    lonlat_to_cell()/cell_to_index() for essentially all points.
  - Fixed is_pentagon() for ISEA aperture 7, which undercounted or threw
    at resolution >= 1; it now decodes each cell's own (i, j) instead of
    relying on an unreliable forward computation.
  - Fixed hex_compact() emitting duplicate cell IDs when the input
    already contained a parent cell alongside all 7 of its children.
  - Fixed hexify_assign()'s Z3 backend discarding the sign of quantized
    (i, j), causing sign-flipped points to collide on the same cell ID.
  - Fixed NaN/Inf inputs reaching undefined behavior in
    snyder_forward()'s sort comparator and silently returning garbage
    from hex quantization instead of erroring.
  - cell_to_index()/get_parent()/get_children() no longer produce a
    confusing internal bound error (or, with a naive fix, silently wrong
    cell IDs) for mixed aperture "4/3" grids. In 0.7.3 these raised a
    clear "not implemented" error; 0.7.4 implements the navigation
    geometrically (see above).
  - hex_extract() now respects cells=/boundary= when grid is a HexData
    object (previously silently ignored).
  - hex_browse()'s data.frame input mode no longer crashes on duplicate
    cell_id rows.
  - plot_globe()'s resolve_center() now validates a named center
    vector's names instead of silently building an invalid PROJ string.
  - Hierarchical-index functions (hexify_cell_to_index() and siblings)
    now validate resolution/aperture like hexify_lonlat_to_cell()
    already did; the C++ layer now enforces the max resolution for
    aperture 3/4 the same way it already did for aperture 7.
  - as_dggrid() now accepts a modern HexGridInfo object, matching
    dgverify().
  - HexData's validity check no longer has a blind spot for empty
    cell_id/cell_center paired with non-empty data.
  - hex_grid() and legacy hexify_grid() now give clear errors for
    non-numeric/NA/non-positive resolution, crs, or area, instead of
    base-R errors or a silent resolution = NaN.
  - plot_globe(exclude_antarctica = TRUE) now warns instead of silently
    no-op'ing for custom land_data without a recognized country-name
    column.
  - import_h3() now drops NA cell IDs (with a warning) when data is
    attached, matching hexify()'s existing NA-coordinate handling.
  - prepare_fill_column() now warns when breaks= is supplied for a
    discrete value column instead of silently discarding it.

Package hygiene

  - Removed dead code (index_to_cell_internal()), an orphaned test
    fixture, and a committed rendered vignette; fixed a native-pipe
    usage that required a newer R than the package declares; fixed stale
    roxygen text; documented HexData['s drop = FALSE default explicitly.
  - C++: replaced a raw malloc/free digit buffer with std::vector; an
    out-of-range digit now throws instead of silently clamping; added a
    resolution/string-length consistency check to z3::decode();
    get_children_indices() no longer swallows unrelated exceptions
    behind a catch-all around the max-resolution boundary check.

Tests

  - Strengthened ~20 hexify_heatmap() tests that previously only checked
    the return type is a ggplot object to assert on the actual plot
    data/mapping (fill column, scale type, colors, bins, limits,
    labels).
  - Added coverage for the Raster*/SpatRaster basemap path and for
    hex_browse()'s value-to-fill-color mapping.

                        Changes in version 0.7.2                        

  - Fixed aperture 4/7 lon/lat-to-index and index-to-lon/lat conversion:
    quantization was operating on raw icosahedron triangle coordinates
    instead of folding into the quad frame first, producing wrong
    indices for any point on triangles numbered 12-19.
  - Implemented cpp_hex_index_z3_quantize_digits(),
    cpp_hex_index_z3_center(), and the Z3 corners helper, which
    previously returned placeholder zeros instead of real quantized
    digits/coordinates.
  - Fixed hex_zonal() row misalignment: results are now keyed off the
    deduplicated hex_sf$cell_id order instead of the pre-dedup input,
    and cells input now drops NA/duplicate values before lookup.
  - Fixed grid_clip() argument order in hex_zonal()'s boundary path.

                        Changes in version 0.7.1                        

Code quality and documentation

Improvements

  - Refactored rcpp_aperture.cpp: extracted parameterized helpers for
    quantize/center/corners, eliminating copy-paste across 3 apertures.
  - Consolidated input validation: conversion functions now use shared
    validate_aperture() / validate_resolution() from constants.R.
  - Optimized is_pentagon() for ISEA grids: computes pentagon cell IDs
    directly from quad coordinates instead of looping through the
    lon/lat pipeline.
  - Stricter aperture parsing in hexify(): rejects unexpected values
    with an informative error instead of silently coercing via
    as.integer().
  - Deduplicated theme_minimal() calls in plot methods with
    .theme_clean() helper.

Documentation

  - Added "Why Hexagonal Grids?" section to README (equal area, uniform
    adjacency, low shape distortion).
  - Added "Known Limitations" section to README (H3 resolution cap,
    pentagons, projection precision).

Tests

  - New test-edge-cases.R with 32 tests covering poles, antimeridian,
    dateline, equator, roundtrip stability, pentagon invariants,
    neighbor symmetry, and resolution-0 cell counts.

                        Changes in version 0.7.0                        

Spatial analysis primitives

Hotfix

  - Fixed aperture 7 cell encoding to use Class III substrate
    quantization.
  - Updated max_cell_id() for aperture 7 bounding box.

New features

  - New get_neighbors(): returns k-ring (disk) of neighboring cells for
    both ISEA and H3 grids. Supports k > 1 for multi-ring expansion,
    distances = TRUE for ring distance output, and vectorized cell
    input. ISEA backend uses axial coordinate offsets with lon/lat
    fallback for cross-quad boundaries. H3 backend uses vendored
    gridDisk / gridDiskDistances / gridRingUnsafe.
  - New hex_summarize(): cell-level data aggregation with tidyeval
    support. Groups by cell_id, applies user-defined summary
    expressions, returns a data.frame with cell centers, areas, and
    point counts. Supports geometry = TRUE for sf output.
  - New C++ bindings: cpp_h3_gridDisk, cpp_h3_gridDiskDistances,
    cpp_h3_gridRingUnsafe, cpp_get_neighbors_isea, cpp_get_neighbors_z7

                 Changes in version 0.6.5 (2026-02-28)                  

  - Fixed empty translation unit warning in vendored H3 h3Assert.c
    (clang 21 -Wempty-translation-unit)

                        Changes in version 0.6.4                        

  - Removed compiled object files (.o) from source tarball that caused
    installation failure on Linux (Debian) and NOTE on all platforms
  - Wrapped plot_globe() examples in \donttest{} to reduce check time
    (was 608s on win-builder)
  - Reworded DESCRIPTION to avoid "vendored" spelling flag

                        Changes in version 0.6.3                        

  - cell_to_sf() now applies sf::st_wrap_dateline() automatically,
    fixing horizontal streaks on flat map projections (Plate Carrée,
    Robinson, etc.) for hexagons crossing the ±180° antimeridian
  - as_sf(x, geometry = "polygon") now routes all grids (ISEA and H3)
    through cell_to_sf(), ensuring consistent antimeridian handling
  - hexify_cell_to_sf() gains antimeridian normalization matching
    cell_to_sf()
  - Vignettes no longer require manual st_wrap_dateline() calls

                        Changes in version 0.6.2                        

Native H3 backend — zero external dependencies

  - Vendored H3 v4.4.1 C source, replacing the h3o R package dependency
  - H3 is now always available — no optional install, no Suggests
  - Uses H3 experimental polygon fill for full spatial coverage in
    hexify()
  - New native C++ bindings: h3_lat_lng_to_cell, h3_cell_to_boundary,
    h3_cell_to_parent, h3_cell_to_children, h3_polygon_to_cells,
    h3_cell_area_km2, h3_cell_to_lat_lng

                        Changes in version 0.6.1                        

  - Warn when aperture is passed with type = "h3" (ignored parameter)
  - Expanded H3 resolution guidance in hex_grid() documentation
  - Extended H3 vignette resolution table to full range (0-15)
  - Added h3_crosswalk() example to H3 vignette

                        Changes in version 0.6.0                        

ISEA–H3 crosswalk and per-cell area

  - New h3_crosswalk(): bidirectional mapping between ISEA and H3 cell
    IDs, with automatic resolution matching and per-cell area comparison
  - New cell_area(): returns geodesic area (km²) for each cell —
    constant for ISEA (equal-area), location-dependent for H3, with
    session-scoped caching
  - HexData $cell_area_km2, [["cell_area_km2"]], and as.data.frame() now
    return per-cell areas for H3 grids instead of the grid-wide average
  - Internal: extracted closest_h3_resolution() helper shared by
    hex_grid() and h3_crosswalk()

                        Changes in version 0.5.0                        

H3 grid support

  - Added H3 (Uber) as a first-class grid type: hex_grid(resolution = 8,
    type = "h3")
  - All core functions work with H3 grids: hexify(), cell_to_sf(),
    grid_rect(), grid_global(), grid_clip(), get_parent(),
    get_children()
  - H3 support requires the h3o package (Suggests, not required for ISEA
    workflows)
  - New hexify_compare_resolutions(type = "h3") for H3 resolution table
  - dgearthstat() now accepts HexGridInfo objects directly
  - New grid_type slot on HexGridInfo: "isea" (default) or "h3"
  - HexData cell_id slot supports character (H3) and numeric (ISEA) cell
    IDs
  - Backward compatible: all existing ISEA workflows unchanged

                 Changes in version 0.3.10 (2026-02-04)                 

Hotfix for geometry issues

  - Fixed invalid pentagon geometries that caused gaps in global grids
  - Fixed antimeridian-crossing polygons using st_wrap_dateline()
  - Added polar cap sampling to grid_global() to include cells above
    ±85° latitude

                        Changes in version 0.3.6                        

  - Reduced test suite runtime for CRAN by skipping detailed consistency
    tests (full tests still run locally via NOT_CRAN=true)
  - Fixed CRAN incoming check NOTE: "Overall checktime 15 min > 10 min"

                        Changes in version 0.3.5                        

  - Simplified plot examples to reduce runtime
  - Added non-standard files to .Rbuildignore
  - Fixed slow example NOTE

                        Changes in version 0.3.4                        

Hotfix for CRAN UBSAN check failure

  - Fixed undefined behavior in Snyder ISEA projection causing NaN
    values (UBSAN error on M1 Mac CRAN check: "nan is outside the range
    of representable values of type 'long long'" at
    coordinate_transforms.cpp:243-244)
  - Root cause: floating-point precision in face assignment could
    project points onto geometrically invalid triangle faces near
    icosahedron edges
  - Solution: added projection validation with face-retry logic - if
    validation fails (z > DH), automatically tries adjacent faces until
    valid

                 Changes in version 0.3.3 (2026-01-20)                  

  - CRAN submission fixes

                        Changes in version 0.3.2                        

  - Documentation improvements
  - Fixed hierarchical index functions
  - Increased test coverage to 90%

                        Changes in version 0.3.1                        

  - Minor bug fixes

                        Changes in version 0.3.0                        

  - Major refactoring of coordinate transformation system
  - Improved performance for large grids

                        Changes in version 0.2.0                        

  - Added dggridR compatibility functions (as_dggrid(), from_dggrid())
  - New hierarchical indexing support with H-index functions
  - Improved coordinate conversion pipeline
  - Added grid statistics functions

                        Changes in version 0.1.0                        

  - Initial release
  - ISEA discrete global grid implementation
  - Support for apertures 3, 4, 7, and mixed 4/3
  - Compatible with dggridR output
