diff --git a/DESCRIPTION b/DESCRIPTION index bdff7e5..642efcf 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: hexify Title: Equal-Area Hex Grids on the 'Snyder' 'ISEA' 'Icosahedron' -Version: 0.7.3.9000 +Version: 0.7.4 Authors@R: person("Gilles", "Colling", , "gilles.colling051@gmail.com", role = c("aut", "cre", "cph"), comment = c(ORCID = "0000-0003-3070-6066")) diff --git a/NEWS.md b/NEWS.md index eafdca2..9bf942c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,12 @@ -# hexify 0.7.4 (development version) - -## New features +# hexify 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 diff --git a/R/grid_helpers.R b/R/grid_helpers.R index c0b7acf..eb6612d 100644 --- a/R/grid_helpers.R +++ b/R/grid_helpers.R @@ -8,7 +8,7 @@ #' Normalize antimeridian-crossing polygon coordinates #' #' Shifts longitudes of a ring's coordinate matrix so an antimeridian-crossing -#' polygon becomes contiguous instead of spanning nearly the full [-180, 180] +#' polygon becomes contiguous instead of spanning nearly the full -180 to 180 #' range. Downstream `sf::st_wrap_dateline()` then splits it correctly for #' flat-map rendering. #' diff --git a/R/hexify_index.R b/R/hexify_index.R index 3e247b1..a9a3b4d 100644 --- a/R/hexify_index.R +++ b/R/hexify_index.R @@ -15,8 +15,8 @@ #' Convert cell coordinates to index string #' -#' Converts DGGRID cell coordinates (face, i, j) to a hierarchical index string. -#' The index type is automatically selected based on aperture unless specified. +#' Converts cell coordinates (`face`, `i`, `j`) to a hierarchical index string. +#' The index type is selected from `aperture` when `index_type = "auto"`. #' #' @param face Face/quad number (0-19) #' @param i I coordinate @@ -25,19 +25,30 @@ #' @param aperture Aperture (3, 4, or 7) #' @param index_type Index encoding: "auto" (default), "z3", "z7", or "zorder" #' -#' @return Index string (e.g., "051223") +#' @return A character vector of index strings. #' #' @details #' Default index types by aperture: #' - Aperture 3: Z3 (optimized digit selection) #' - Aperture 4: Z-order (Morton curve) -#' - Aperture 7: Z7 (hierarchical with Class III handling) +#' - Aperture 7: Z7 (one base-7 child digit per resolution) +#' +#' A Z7 index has the form `BBd1...dr`, where `BB` is the two-digit base +#' cell (00--11), `r` is the resolution, and every child digit is in 0--6. +#' hexify's Z7 encoding is bijective: decoding and re-encoding a valid index +#' returns the same string. It follows DGGRID's Z7 layout for ordinary cells, +#' but retains distinct indices in pentagon regions where DGGRID's encoder can +#' map different cells to the same string. #' #' @family hierarchical index #' @keywords internal #' @export -#' @examples -#' idx <- hexify_cell_to_index(5, 10, 15, resolution = 3, aperture = 3) +#' @examples +#' idx <- hexify_cell_to_index(5, 10, 15, resolution = 3, aperture = 3) +#' +#' # Aperture-7 indices contain a two-digit base cell and one digit per level +#' z7 <- hexify_cell_to_index(5, 1, 1, resolution = 2, aperture = 7) +#' nchar(z7) == 4L hexify_cell_to_index <- function(face, i, j, resolution, aperture = 3L, index_type = c("auto", "z3", "z7", "zorder")) { index_type <- match.arg(index_type) @@ -51,19 +62,26 @@ hexify_cell_to_index <- function(face, i, j, resolution, aperture = 3L, #' Convert index string to cell coordinates #' -#' Decodes a hierarchical index string back to cell coordinates. +#' Decodes a hierarchical index string back to its cell coordinates and +#' resolution. For Z7, valid indices round-trip exactly through +#' [hexify_cell_to_index()]. #' #' @param index Index string #' @param aperture Aperture (3, 4, or 7) #' @param index_type Index encoding used. Default "auto" infers from aperture. #' -#' @return List with face, i, j, and resolution +#' @return A list with `face`, `i`, `j`, and `resolution`. #' #' @family hierarchical index #' @keywords internal #' @export -#' @examples -#' cell <- hexify_index_to_cell("0012012", aperture = 3) +#' @examples +#' cell <- hexify_index_to_cell("0012012", aperture = 3) +#' +#' z7_cell <- hexify_index_to_cell("110001", aperture = 7) +#' hexify_cell_to_index(z7_cell$face, z7_cell$i, z7_cell$j, +#' z7_cell$resolution, aperture = 7 +#' ) hexify_index_to_cell <- function(index, aperture = 3L, index_type = c("auto", "z3", "z7", "zorder")) { index_type <- match.arg(index_type) @@ -73,7 +91,8 @@ hexify_index_to_cell <- function(index, aperture = 3L, #' Convert longitude/latitude to index string #' -#' Main entry point for geocoding points to grid cells. +#' Projects geographic coordinates to grid cells and returns their hierarchical +#' index strings. Inputs are vectorized over `lon` and `lat`. #' #' @param lon Longitude in degrees #' @param lat Latitude in degrees @@ -81,13 +100,16 @@ hexify_index_to_cell <- function(index, aperture = 3L, #' @param aperture Aperture (3, 4, or 7) #' @param index_type Index encoding: "auto" (default), "z3", "z7", or "zorder" #' -#' @return Index string +#' @return A character vector of index strings. #' #' @family hierarchical index #' @keywords internal #' @export -#' @examples -#' idx <- hexify_lonlat_to_index(16.37, 48.21, resolution = 5, aperture = 3) +#' @examples +#' idx <- hexify_lonlat_to_index(16.37, 48.21, resolution = 5, aperture = 3) +#' idx7 <- hexify_lonlat_to_index(16.37, 48.21, +#' resolution = 4, aperture = 7 +#' ) hexify_lonlat_to_index <- function(lon, lat, resolution, aperture = 3L, index_type = c("auto", "z3", "z7", "zorder")) { index_type <- match.arg(index_type) @@ -99,13 +121,15 @@ hexify_lonlat_to_index <- function(lon, lat, resolution, aperture = 3L, #' Convert index string to longitude/latitude #' -#' Returns the cell center coordinates for a given index. +#' Returns the geographic coordinates of an indexed cell's center. This is a +#' cell-level inverse of [hexify_lonlat_to_index()]: the returned point is the +#' center, not necessarily the original input point. #' #' @param index Index string #' @param aperture Aperture (3, 4, or 7) #' @param index_type Index encoding. Default "auto" infers from aperture. #' -#' @return Named numeric vector with lon and lat in degrees +#' @return A named numeric vector with `lon` and `lat` in degrees. #' #' @family hierarchical index #' @keywords internal @@ -292,22 +316,31 @@ hexify_cell_id_to_quad_ij <- function(cell_id, resolution, aperture) { #' Get canonical form of Z7 index #' -#' For Z7 indices that form cycles during decode/encode, returns the -#' lexicographically smallest index in the cycle. Provides stable -#' unique identifiers for aperture 7 grids. +#' Decodes and re-encodes a Z7 index until it reaches a stable form. Current Z7 +#' indices are bijective, so every valid index is already canonical and this +#' function normally returns its input unchanged. It remains available for +#' validating or normalizing indices created by older hexify versions. #' -#' @param index Z7 index string -#' @param max_iterations Maximum iterations for cycle detection (default 128) +#' @param index A length-one Z7 index string. +#' @param max_iterations Maximum number of decode/encode iterations. This is a +#' safety bound for legacy indices; the default is 128. #' -#' @return Canonical form (lexicographically smallest in cycle) +#' @return A length-one character string containing the stable index. #' #' @family hierarchical index #' @keywords internal #' @export #' @examples -#' # These all return the same canonical form -#' hexify_z7_canonical("110001") -#' hexify_z7_canonical("110002") +#' # Valid Z7 indices are stable +#' hexify_z7_canonical("110001") +#' +#' cell <- hexify_index_to_cell("110001", aperture = 7) +#' identical( +#' hexify_cell_to_index(cell$face, cell$i, cell$j, +#' cell$resolution, aperture = 7 +#' ), +#' "110001" +#' ) hexify_z7_canonical <- function(index, max_iterations = 128L) { cpp_z7_canonical_form(as.character(index), as.integer(max_iterations)) } diff --git a/man/hexify_cell_to_index.Rd b/man/hexify_cell_to_index.Rd index 1066407..092d5fc 100644 --- a/man/hexify_cell_to_index.Rd +++ b/man/hexify_cell_to_index.Rd @@ -27,22 +27,33 @@ hexify_cell_to_index( \item{index_type}{Index encoding: "auto" (default), "z3", "z7", or "zorder"} } \value{ -Index string (e.g., "051223") +A character vector of index strings. } \description{ -Converts DGGRID cell coordinates (face, i, j) to a hierarchical index string. -The index type is automatically selected based on aperture unless specified. +Converts cell coordinates (\code{face}, \code{i}, \code{j}) to a hierarchical index string. +The index type is selected from \code{aperture} when \code{index_type = "auto"}. } \details{ Default index types by aperture: \itemize{ \item Aperture 3: Z3 (optimized digit selection) \item Aperture 4: Z-order (Morton curve) -\item Aperture 7: Z7 (hierarchical with Class III handling) +\item Aperture 7: Z7 (one base-7 child digit per resolution) } + +A Z7 index has the form \code{BBd1...dr}, where \code{BB} is the two-digit base +cell (00--11), \code{r} is the resolution, and every child digit is in 0--6. +hexify's Z7 encoding is bijective: decoding and re-encoding a valid index +returns the same string. It follows DGGRID's Z7 layout for ordinary cells, +but retains distinct indices in pentagon regions where DGGRID's encoder can +map different cells to the same string. } \examples{ idx <- hexify_cell_to_index(5, 10, 15, resolution = 3, aperture = 3) + +# Aperture-7 indices contain a two-digit base cell and one digit per level +z7 <- hexify_cell_to_index(5, 1, 1, resolution = 2, aperture = 7) +nchar(z7) == 4L } \seealso{ Other hierarchical index: diff --git a/man/hexify_index_to_cell.Rd b/man/hexify_index_to_cell.Rd index c591223..6f9edd1 100644 --- a/man/hexify_index_to_cell.Rd +++ b/man/hexify_index_to_cell.Rd @@ -18,13 +18,20 @@ hexify_index_to_cell( \item{index_type}{Index encoding used. Default "auto" infers from aperture.} } \value{ -List with face, i, j, and resolution +A list with \code{face}, \code{i}, \code{j}, and \code{resolution}. } \description{ -Decodes a hierarchical index string back to cell coordinates. +Decodes a hierarchical index string back to its cell coordinates and +resolution. For Z7, valid indices round-trip exactly through +\code{\link[=hexify_cell_to_index]{hexify_cell_to_index()}}. } \examples{ cell <- hexify_index_to_cell("0012012", aperture = 3) + +z7_cell <- hexify_index_to_cell("110001", aperture = 7) +hexify_cell_to_index(z7_cell$face, z7_cell$i, z7_cell$j, + z7_cell$resolution, aperture = 7 +) } \seealso{ Other hierarchical index: diff --git a/man/hexify_index_to_lonlat.Rd b/man/hexify_index_to_lonlat.Rd index 065499f..87bffdf 100644 --- a/man/hexify_index_to_lonlat.Rd +++ b/man/hexify_index_to_lonlat.Rd @@ -18,10 +18,12 @@ hexify_index_to_lonlat( \item{index_type}{Index encoding. Default "auto" infers from aperture.} } \value{ -Named numeric vector with lon and lat in degrees +A named numeric vector with \code{lon} and \code{lat} in degrees. } \description{ -Returns the cell center coordinates for a given index. +Returns the geographic coordinates of an indexed cell's center. This is a +cell-level inverse of \code{\link[=hexify_lonlat_to_index]{hexify_lonlat_to_index()}}: the returned point is the +center, not necessarily the original input point. } \examples{ coords <- hexify_index_to_lonlat("0012012", aperture = 3) diff --git a/man/hexify_lonlat_to_index.Rd b/man/hexify_lonlat_to_index.Rd index 297e967..4aea58a 100644 --- a/man/hexify_lonlat_to_index.Rd +++ b/man/hexify_lonlat_to_index.Rd @@ -24,13 +24,17 @@ hexify_lonlat_to_index( \item{index_type}{Index encoding: "auto" (default), "z3", "z7", or "zorder"} } \value{ -Index string +A character vector of index strings. } \description{ -Main entry point for geocoding points to grid cells. +Projects geographic coordinates to grid cells and returns their hierarchical +index strings. Inputs are vectorized over \code{lon} and \code{lat}. } \examples{ idx <- hexify_lonlat_to_index(16.37, 48.21, resolution = 5, aperture = 3) +idx7 <- hexify_lonlat_to_index(16.37, 48.21, + resolution = 4, aperture = 7 +) } \seealso{ Other hierarchical index: diff --git a/man/hexify_z7_canonical.Rd b/man/hexify_z7_canonical.Rd index aaf65df..48a6c33 100644 --- a/man/hexify_z7_canonical.Rd +++ b/man/hexify_z7_canonical.Rd @@ -7,22 +7,31 @@ hexify_z7_canonical(index, max_iterations = 128L) } \arguments{ -\item{index}{Z7 index string} +\item{index}{A length-one Z7 index string.} -\item{max_iterations}{Maximum iterations for cycle detection (default 128)} +\item{max_iterations}{Maximum number of decode/encode iterations. This is a +safety bound for legacy indices; the default is 128.} } \value{ -Canonical form (lexicographically smallest in cycle) +A length-one character string containing the stable index. } \description{ -For Z7 indices that form cycles during decode/encode, returns the -lexicographically smallest index in the cycle. Provides stable -unique identifiers for aperture 7 grids. +Decodes and re-encodes a Z7 index until it reaches a stable form. Current Z7 +indices are bijective, so every valid index is already canonical and this +function normally returns its input unchanged. It remains available for +validating or normalizing indices created by older hexify versions. } \examples{ -# These all return the same canonical form +# Valid Z7 indices are stable hexify_z7_canonical("110001") -hexify_z7_canonical("110002") + +cell <- hexify_index_to_cell("110001", aperture = 7) +identical( + hexify_cell_to_index(cell$face, cell$i, cell$j, + cell$resolution, aperture = 7 + ), + "110001" +) } \seealso{ Other hierarchical index: diff --git a/src/index_z7.cpp b/src/index_z7.cpp index 22a6feb..78c7626 100644 --- a/src/index_z7.cpp +++ b/src/index_z7.cpp @@ -492,8 +492,8 @@ std::string canonical_form(const std::string& z7_index, int max_iterations) { long long i, j; int res = current.length() - 2; - decode(current, res, quadNum, i, j); - std::string next = encode(quadNum, i, j, res); + decode_bijective(current, res, quadNum, i, j); + std::string next = encode_bijective(quadNum, i, j, res); // Check for fixed point if (next == current) { diff --git a/tests/testthat/fixtures/test_cache.rds b/tests/testthat/fixtures/test_cache.rds index 7592361..b304033 100644 Binary files a/tests/testthat/fixtures/test_cache.rds and b/tests/testthat/fixtures/test_cache.rds differ diff --git a/tests/testthat/generate_cache.R b/tests/testthat/generate_cache.R index 3ce67d5..a7b51dd 100644 --- a/tests/testthat/generate_cache.R +++ b/tests/testthat/generate_cache.R @@ -41,7 +41,7 @@ coords7 <- hexify_cell_to_lonlat(cells7, resolution = 3, aperture = 7) cache$cells_ap7 <- list(cells = cells7, lon = coords7$lon_deg, lat = coords7$lat_deg) # Projection forward -proj <- cpp_snyder_forward(10, 45) +proj <- hexify:::cpp_snyder_forward(10, 45) cache$proj_forward <- as.list(proj) # Face centers diff --git a/tests/testthat/test-index-z7.R b/tests/testthat/test-index-z7.R index 137be9c..03f0087 100644 --- a/tests/testthat/test-index-z7.R +++ b/tests/testthat/test-index-z7.R @@ -1,7 +1,7 @@ # test-index_z7.R # Tests for Z7 encoding/decoding -# Updated to match DGGRID's exact behavior for aperture 7 +# Tests for hexify's bijective aperture-7 index. library(testthat) @@ -103,24 +103,18 @@ test_that("Z7: Pentagon rotation behavior follows DGGRID", { # Exact result depends on DGGRID implementation }) -test_that("Z7: Multi-digit indices match DGGRID behavior", { - # Test multi-digit indices match DGGRID's exact behavior - # Some indices don't round-trip due to aperture 7 cross-face remapping +test_that("Z7: Multi-digit indices round-trip", { test_cases <- list( - # Most round-trip correctly list(idx = "0111111", expected = "0111111"), - # Odd-resolution (Class III) case: the encoder feeds z7 the Class I - # substrate Q2DI, so this canonicalizes to a fixed point at odd res. - list(idx = "0222222", expected = "0316104"), + list(idx = "0222222", expected = "0222222"), list(idx = "0333333", expected = "0333333"), list(idx = "0400000", expected = "0400000"), list(idx = "0511111", expected = "0511111"), list(idx = "0622222", expected = "0622222"), list(idx = "0733333", expected = "0733333"), list(idx = "0844444", expected = "0844444"), - # This one doesn't round-trip - rotates 5s to 1s - list(idx = "0955555", expected = "0911111"), + list(idx = "0955555", expected = "0955555"), list(idx = "1066666", expected = "1066666") ) @@ -129,7 +123,7 @@ test_that("Z7: Multi-digit indices match DGGRID behavior", { idx2 <- hexify_cell_to_index(result$face, result$i, result$j, result$resolution, 7L, "z7") expect_equal(idx2, tc$expected, - info = sprintf("Index %s should encode to %s (DGGRID behavior)", + info = sprintf("Index %s should round-trip to %s", tc$idx, tc$expected)) } }) @@ -158,19 +152,12 @@ test_that("Z7: Parent-child relationships work", { as.integer(parent_result$resolution) + 1L, info = sprintf("Child %s resolution check", child_idx)) - # Re-encoding child may not give same index due to DGGRID behavior + # Every child has a unique, stable index. child_idx2 <- hexify_cell_to_index(child_result$face, child_result$i, child_result$j, child_result$resolution, 7L, "z7") - # For specific known cases, check expected behavior - if (child_idx == "012") { - expect_equal(child_idx2, "016", - info = "Child 012 should re-encode to 016 (DGGRID behavior)") - } else if (child_idx == "022") { - expect_equal(child_idx2, "026", - info = "Child 022 should re-encode to 026 (DGGRID behavior)") - } - # Most other children should round-trip correctly + expect_equal(child_idx2, child_idx, + info = sprintf("Child %s should round-trip", child_idx)) } } } @@ -191,11 +178,11 @@ test_that("Z7: Edge cases are handled correctly", { # Empty string should fail expect_error(hexify_index_to_cell("", 7L, "z7")) - # Negative coordinates should work (they get transformed) + # Arbitrary integer coordinates should round-trip without crashing. idx_neg <- hexify_cell_to_index(5L, -1L, -1L, 1L, 7L, "z7") result_neg <- hexify_index_to_cell(idx_neg, 7L, "z7") - expect_true(result_neg$i >= 0L || result_neg$j >= 0L, - info = "Negative coords should be handled") + expect_equal(c(result_neg$i, result_neg$j), c(-1, -1), + info = "Negative coords should round-trip") }) test_that("Z7: Resolution progression works correctly", { @@ -217,52 +204,21 @@ test_that("Z7: Resolution progression works correctly", { expect_equal(nchar(idx_r3), 2L + 3L) # Face + 3 digits }) -test_that("Z7: Known problem indices behave as expected", { - # Test specific indices that we know don't round-trip - # These are not bugs but expected behavior for aperture 7 - - # Face 1 with digit 2 should re-encode to digit 6 after remapping - result <- hexify_index_to_cell("012", 7L, "z7") - re_encoded <- hexify_cell_to_index(result$face, result$i, result$j, - result$resolution, 7L, "z7") - expect_equal(re_encoded, "016", - info = "012 should re-encode to 016 (DGGRID behavior)") - - # The 110001 cycle - each re-encodes to the next in cycle - cycle_test <- list( - list(idx = "110001", expected = "110002"), - list(idx = "110002", expected = "110004"), - list(idx = "110004", expected = "110006"), - list(idx = "110006", expected = "110001") # Completes cycle - ) - - for (tc in cycle_test) { - result <- hexify_index_to_cell(tc$idx, 7L, "z7") +test_that("Z7: Former DGGRID problem indices are stable", { + problem_indices <- c("012", "110001", "110002", "110004", "110006") + + for (idx in problem_indices) { + result <- hexify_index_to_cell(idx, 7L, "z7") re_encoded <- hexify_cell_to_index(result$face, result$i, result$j, result$resolution, 7L, "z7") - expect_equal(re_encoded, tc$expected, - info = sprintf("%s should re-encode to %s (part of cycle)", - tc$idx, tc$expected)) + expect_equal(re_encoded, idx, + info = sprintf("%s should round-trip", idx)) } }) test_that("Z7: Canonical forms provide stability", { - # Test canonical forms - provides stable unique identifiers for cells - - # The 110001 cycle - all should have same canonical form - cycle_indices <- c("110001", "110002", "110004", "110006") - canonicals <- vapply(cycle_indices, hexify_z7_canonical, character(1)) - expect_true(all(canonicals == "110001"), - info = "All cycle members should canonicalize to 110001") - - # Test other known transformations - expect_equal(hexify_z7_canonical("012"), "016", - info = "012 should canonicalize to 016") - expect_equal(hexify_z7_canonical("0955555"), "0911111", - info = "0955555 should canonicalize to 0911111") - - # Canonical forms should be stable - for (idx in c("110001", "016", "0911111")) { + for (idx in c("012", "110001", "110002", "110004", "110006", "0955555")) { + expect_equal(hexify_z7_canonical(idx), idx) canonical <- hexify_z7_canonical(idx) result <- hexify_index_to_cell(canonical, 7L, "z7") re_encoded <- hexify_cell_to_index(result$face, result$i, result$j, diff --git a/vignettes/theory.Rmd b/vignettes/theory.Rmd index b725c94..ec148cb 100644 --- a/vignettes/theory.Rmd +++ b/vignettes/theory.Rmd @@ -595,7 +595,18 @@ where **BB** is the base cell (00--11) and each digit $D_k \in \{0, 1, \ldots, 6 | 5 | IK_AXES | IK-axis direction | | 6 | IJ_AXES | IJ-axis direction | -Pentagon cells (at icosahedron vertices) have only 5 children instead of 7. Base cells 0--5 skip digit 2 (J_AXES); cells 6--11 skip digit 5 (IK_AXES). +The first two characters always identify the base cell, so the resolution is +`nchar(index) - 2`. Dropping the final digit gives the parent; appending digits +0--6 enumerates the seven positions in the next refinement level. + +hexify uses a **bijective Z7 variant**. DGGRID's base-cell reassignment and +pentagon digit-skip rules can make its encoder non-injective near pentagons: +distinct cells may receive the same string. hexify keeps the geographic quad +fixed in those regions, giving every cell a distinct index and guaranteeing +`index -> cell -> index` round-trips. Away from those pentagon-region cases, +the strings follow the DGGRID Z7 layout. This distinction matters when +exchanging raw Z7 strings with DGGRID; geographic coordinates and cell geometry +remain the safest interoperability layer. ```{r z7-example} # Z7 index encoding for aperture 7 @@ -612,6 +623,14 @@ parent_info <- hexify_index_to_cell(parent_idx, 7, "z7") cat(sprintf(" Parent index: %s (face %d, i=%d, j=%d)\n", parent_idx, parent_info$face, as.integer(parent_info$i), as.integer(parent_info$j))) + +# A valid Z7 index decodes and re-encodes without changing +decoded <- hexify_index_to_cell(idx, 7, "z7") +stopifnot(identical( + hexify_cell_to_index(decoded$face, decoded$i, decoded$j, + decoded$resolution, 7, "z7"), + idx +)) ``` #### Z3 Index (Aperture 3) @@ -662,7 +681,11 @@ cat(sprintf("Aperture 4: Cell %d -> Z-order index: %s\n", cell, idx)) | **Parent operation** | Drop last digit | Drop last pair | Drop last digit(s) | | **Index length** (res $r$) | $2 + r$ | $2 + 2\lceil r/2\rceil$ | $2 + r$ or $2 + 2r$ | -All three encodings are bijective: each $(quad, i, j)$ triple maps to exactly one index string, and vice versa. hexify stores indices as character strings to support arbitrary precision and avoid integer overflow at high resolutions. +All three hexify encodings are bijective: each valid $(quad, i, j)$ cell maps +to exactly one index string, and vice versa. hexify stores indices as character +strings to support arbitrary precision and avoid integer overflow at high +resolutions. As noted above, hexify's bijective Z7 strings intentionally differ +from DGGRID in the pentagon regions where DGGRID's raw Z7 encoding collides. ### SEQNUM: The Flat Cell ID