DuckDB IGEO7/Z7 Extension

April 28, 2026 · View on GitHub

A DuckDB loadable extension providing native scalar functions for the IGEO7 (Z7) hexagonal discrete global grid system. Wraps the C++ Z7 library for high-performance bit-level index manipulation and neighbour traversal directly inside DuckDB queries.

This extension codes should be perfect synergy with: am2222/duckdb-dggs

Overview: A Hierarchically Indexed Hexagonal Equal-Area Discrete Global Grid System

Hexagonal Discrete Global Grid Systems (DGGS) offer significant advantages for spatial analysis due to their uniform cell shapes and efficient indexing. Among the three central place apertures (3, 4, and 7), aperture 7 subdivisions exhibit very desirable properties, including the preservation of hexagonal symmetry and the formation of unambiguous indexing hierarchies.

Interest in hierarchically indexed aperture 7 hexagonal DGGS has recently increased due to the popularity of the H3 DGGS. But there are currently no open-source equal-area aperture 7 hexagonal DGGS available, that provide similar indexing capabilities like H3.

We present IGEO7, a novel pure aperture 7 hexagonal DGGS, and Z7, its associated hierarchical integer indexing system. In contrast to H3, where cell sizes vary by up to ±50% across the globe, IGEO7 uses cells of equal area, making it a true equal-area DGGS.

IGEO7 and Z7 are implemented in the open-source software DGGRID. We also present a use case for on-demand suitability modeling to demonstrate a practical application of this new DGGS. Practical information and sphere vs ellipsoid

The original IGEO7 implementation is available in the DGGRID software since version 8.41 and is a ISEA7H type DGGS with the new Z7 indexing system.

Recent developments in the OGC API DGGS standard draft make reference to refinement ratio 7 DGGRS with ISEA projection. In order to have DGGH and ZIRS compliant with the OGC DGGS standard, we aim to enable a few minor adjustments to IGEO7::

  • to apply authalic conversion to the WGS84 ellipsoid instead of the spherical approximation (based off geographiclib)
  • a rotation of the base icosahedron of 0.05 degrees from 11.25 to 11.2 to align the vertices better with water bodies through a specific parameter in DGGRID/duck-dggs, e.g. dggs_vert0_lon / pole_lon_deg = 11.2

Background (wrenoud/Z7/ and IGEO7)

This is a work-in-progress to explore the possibility of neighbor traversal in the Z7 domain. Z7 is an indexing system for aperature 7 hexagonal discrete global grid systems, as coined in Kmoch et al. (2025). It is based on the Generalized Balanced Ternary (GBT) numeral system described in Lucas, Gibson (1982), van Roessel (1988), and Wikipedia (2025).

Rotation Pattern

Lucas and Gibson (1982) gave examples of GBT with exclusively counter-clockwise (CCW) rotation. The disadvantage of this approach is that the orientation of the hexagons at the different hierarchy levels quickly diverge from each other. White et al. (1992) proposed an alternating rotation direction (CW, CCW, CW, ...) to maintain an alignment of hexagon orientations across hierarchy levels. Kmoch et al. (2025) adopted this alternating rotation pattern for Z7 assigning CW to odd resolutions and CCW to even resolutions.

  1. Kmoch, A., Sahr, K., Chan, W. T., & Uuemaa, E. (2025). IGEO7: A new hierarchically indexed hexagonal equal-area discrete global grid system. AGILE: GIScience Series, 6(32). https://doi.org/10.5194/agile-giss-6-32-2025
  2. Lucas, D., & Gibson, L. (1982). Automated Analysis of imagery. Air Force Office of Scientific Research. https://apps.dtic.mil/sti/tr/pdf/ADA125710.pdf. (No. AFOSRTR830055).
  3. Sahr, K. (2011). Hexagonal discrete global grid systems for geospatial computing. Archives of Photogrammetry Cartography and Remote Sensing, 22, 363–376.
  4. van Roessel, J. W. (1988). Conversion of Cartesian coordinates from and to Generalized Balanced Ternary addresses. Photogrammetric Engineering & Remote Sensing, 54(11), 1565–1570.
  5. White, D., Kimerling, J. A., & Overton, S. W. (1992). Cartographic and Geometric Components of a Global Sampling Design for Environmental Monitoring. Cartography and Geographic Information Systems, 19(1), 5-22. https://doi.org/10.1559/152304092783786636
  6. Wikipedia Contributors. (2025, December 16). Generalized balanced ternary. Wikipedia; Wikimedia Foundation.

Index format

Z7 packs every cell into a single UBIGINT:

bits [63:60]  base cell  (4 bits, values 0–11)
bits [59:57]  digit 1    (3 bits, values 0–6; 7 = padding / unused slot)
bits [56:54]  digit 2
...
bits  [2:0]   digit 20

The compact string form (igeo7_to_string) is a 2-digit zero-padded base cell followed by the digit characters, stopping before the first 7: 0800432 → base 08, digits 0,0,4,3,2, resolution 5.

The verbose debug form (igeo7_decode_str, SQL macro) always shows all 20 digit slots separated by dots: 08-0.0.4.3.2.7.7.7.7.7.7.7.7.7.7.7.7.7.7.7.


C++ extension functions

FunctionSignatureDescription
igeo7_from_string(VARCHAR) → UBIGINTParse Z7 string to packed index
igeo7_to_string(UBIGINT) → VARCHARCompact Z7 string (stops before first 7)
igeo7_encode(base, d1…d20) → UBIGINTPack base cell + 20 digit slots; accepts UTINYINT or INTEGER literals
igeo7_get_resolution(UBIGINT) → INTEGERResolution (0–20) of an index
igeo7_get_base_cell(UBIGINT) → UTINYINTBase cell ID (0–11)
igeo7_get_digit(UBIGINT, INTEGER) → UTINYINTExtract the i-th digit (1–20)
igeo7_parent(UBIGINT) → UBIGINTParent index (one level up)
igeo7_parent_at(UBIGINT, INTEGER) → UBIGINTAncestor at specific resolution
igeo7_get_neighbours(UBIGINT) → UBIGINT[]All 6 neighbours (invalid neighbours = UINT64_MAX)
igeo7_get_neighbour(UBIGINT, INTEGER) → UBIGINTSingle neighbour by direction (1–6)
igeo7_first_non_zero(UBIGINT) → INTEGERPosition of first non-zero digit slot
igeo7_is_valid(UBIGINT) → BOOLEANFALSE when index equals the invalid sentinel (UINT64_MAX)
igeo7_geo_to_authalic(GEOMETRY) → GEOMETRYConvert geodetic latitudes to authalic latitudes (WGS84)
igeo7_authalic_to_geo(GEOMETRY) → GEOMETRYConvert authalic latitudes back to geodetic (WGS84)

SQL companion macros (sql/igeo7_macros.sql)

Source this file after loading the extension. These macros add convenience operations that compose or extend the C++ functions above.

MacroSignatureDescription
igeo7_decode_str(UBIGINT) → VARCHARVerbose base-d1.d2…d20 form showing all 20 digit slots
igeo7_encode_at_resolution(base, res, d1…d20) → UBIGINTEncode then truncate to resolution (igeo7_encode + igeo7_parent_at)
igeo7_string_parent(VARCHAR) → VARCHARDrop the last character of a compact Z7 string
igeo7_string_local_pos(VARCHAR) → VARCHARLast character of a compact Z7 string (local position digit)
igeo7_string_is_center(VARCHAR) → BOOLEANTrue when the last digit is '0' (center of parent)

Prerequisites

  • CMake ≥ 3.14
  • C++17 compiler (GCC ≥ 9, Clang ≥ 10, MSVC ≥ 19.20)
  • DuckDB v1.5.1 source tree (headers + built library + platform file)

Building

Step 1: Get DuckDB

Clone and build DuckDB at the matching tag. The build produces the headers, shared library, and duckdb_platform_out file needed for the extension metadata footer.

git clone --branch v1.5.1 https://github.com/duckdb/duckdb.git
cd duckdb
BUILD_EXTENSIONS="" make release
cd ..

Step 2: Build the extension

Always pass an absolute path to -DDUCKDB_DIR. A relative path is resolved against the CMake source directory (not the build directory) and can silently pick up the wrong DuckDB installation.

If DuckDB was cloned inside duckdb_igeo7/ (the layout in this repo):

cd duckdb_igeo7
mkdir build && cd build

cmake .. -DDUCKDB_DIR=$(pwd)/../duckdb \
         -DCMAKE_BUILD_TYPE=Release

make -j$(nproc)      # Linux / macOS: nproc or sysctl -n hw.logicalcpu

For a DuckDB checkout elsewhere, use its absolute path directly:

cmake .. -DDUCKDB_DIR=/absolute/path/to/duckdb \
         -DCMAKE_BUILD_TYPE=Release

This produces build/igeo7.duckdb_extension with the required binary metadata footer automatically appended post-build.

Build the standalone Z7 and Authalic tests (no DuckDB required)

cmake .. -DBUILD_TESTS=ON -DDUCKDB_DIR=$(pwd)/../duckdb
make igeo7_test auth_test
./igeo7_test
./auth_test

Loading

DuckDB v1.5.1 only loads files ending in .duckdb_extension. Because this extension is not signed by the DuckDB team, pass -unsigned when starting the CLI (or run SET allow_unsigned_extensions = true; inside an existing session).

# One-liner from the duckdb_igeo7/ directory
duckdb/build/release/duckdb -unsigned \
  -c "LOAD '$(pwd)/build/igeo7.duckdb_extension'; SELECT igeo7_to_string(igeo7_from_string('0800432'));"

Authalic latitude conversion

The conversion follows Karney (2022) for high-performance, high-precision mapping between geodetic (ϕ\phi) and authalic (ξ\xi) latitudes on the WGS84 ellipsoid. It operates on all vertices of a GEOMETRY object.

-- Convert a point from geodetic to authalic
SELECT igeo7_geo_to_authalic('POINT (0 45)'::GEOMETRY);
-- POINT (0 44.87170287343394)

-- Round-trip conversion
SELECT igeo7_authalic_to_geo(igeo7_geo_to_authalic('POINT (0 45)'::GEOMETRY));
-- POINT (0 45)

Inside a session or SQL script:

LOAD '/absolute/path/to/build/igeo7.duckdb_extension';

Loading the SQL companion macros

Pipe the LOAD statement and the macro file together:

cat <(echo "LOAD 'build/igeo7.duckdb_extension';") sql/igeo7_macros.sql \
  | duckdb/build/release/duckdb -unsigned

Or in the interactive shell, source them with .read after loading the extension:

.read sql/igeo7_macros.sql

Usage examples

String ↔ integer round-trip

SELECT igeo7_to_string(igeo7_from_string('0800432'));
-- '0800432'

SELECT igeo7_get_resolution(igeo7_from_string('0800432'));
-- 5

SELECT igeo7_get_base_cell(igeo7_from_string('0800432'));
-- 8

Inspect a cell by its packed integer ID

igeo7_to_string produces the compact form; igeo7_decode_str (SQL macro) produces the verbose dot-separated form showing all 20 slots.

-- compact form
SELECT
    32023330408103935::UBIGINT    AS cell_id,
    igeo7_get_base_cell(cell_id)  AS base,
    igeo7_get_resolution(cell_id) AS res,
    igeo7_to_string(cell_id)      AS compact;

-- ┌───────────────────┬──────┬─────┬─────────────────┐
-- │      cell_id      │ base │ res │     compact     │
-- │      uint64       │ uint8│ int │     varchar     │
-- ├───────────────────┼──────┼─────┼─────────────────┤
-- │ 32023330408103935 │    0 │  13 │ 000161612062413 │
-- └───────────────────┴──────┴─────┴─────────────────┘

-- verbose form (requires sql/igeo7_macros.sql to be sourced first)
SELECT igeo7_decode_str(32023330408103935::UBIGINT);
-- '0-0.1.6.1.6.1.2.0.6.2.4.1.3.7.7.7.7.7.7.7'

Encode a cell from its components

igeo7_encode is the inverse of igeo7_get_base_cell + igeo7_get_digit. Integer literals are accepted without explicit casts. Use 7 for every padding slot beyond the target resolution.

-- Reconstruct '0800432': base=8, resolution=5, digits 0,0,4,3,2
SELECT igeo7_to_string(
    igeo7_encode(8, 0,0,4,3,2, 7,7,7,7,7, 7,7,7,7,7, 7,7,7,7,7)
);
-- '0800432'

-- Must exactly equal igeo7_from_string for the same cell
SELECT igeo7_encode(8, 0,0,4,3,2, 7,7,7,7,7, 7,7,7,7,7, 7,7,7,7,7)
     = igeo7_from_string('0800432') AS equal;
-- true

-- Known cell round-trip: base=0, resolution=13
SELECT (igeo7_encode(0, 0,1,6,1,6,1,2,0,6,2,4,1,3, 7,7,7,7,7,7,7)
        = 32023330408103935::UBIGINT)::BOOLEAN AS same;
-- true

Extract individual digits

SELECT
    igeo7_get_digit(igeo7_from_string('0800432'), 1) AS d1,  -- 0
    igeo7_get_digit(igeo7_from_string('0800432'), 2) AS d2,  -- 0
    igeo7_get_digit(igeo7_from_string('0800432'), 3) AS d3,  -- 4
    igeo7_get_digit(igeo7_from_string('0800432'), 4) AS d4,  -- 3
    igeo7_get_digit(igeo7_from_string('0800432'), 5) AS d5;  -- 2

Parent / ancestor traversal

-- One level up
SELECT igeo7_to_string(igeo7_parent(igeo7_from_string('0800432')));
-- '080043'

-- Ancestor at resolution 3 (keeps digits 1–3, fills the rest with 7)
SELECT igeo7_to_string(igeo7_parent_at(igeo7_from_string('0800432'), 3));
-- '08004'

Neighbours

-- All 6 neighbours as compact strings
SELECT igeo7_to_string(unnest(igeo7_get_neighbours(igeo7_from_string('0800432'))));
-- 0800433, 0800651, 0800064, 0800436, 0800430, 0800655

-- Single neighbour by direction (1–6); invalid neighbours = UINT64_MAX
SELECT igeo7_to_string(igeo7_get_neighbour(igeo7_from_string('0800432'), 1));
-- '0800433'

-- Pentagon check: one direction is always invalid at resolution 2
SELECT
    generate_series                                                            AS dir,
    igeo7_to_string(igeo7_get_neighbour(igeo7_from_string('0800'), generate_series::INTEGER)) AS neighbour,
    igeo7_is_valid(igeo7_get_neighbour(igeo7_from_string('0800'), generate_series::INTEGER))  AS valid
FROM generate_series(1, 6);

Resolution hierarchy walk (recursive CTE)

WITH RECURSIVE hierarchy AS (
    SELECT igeo7_from_string('0800432') AS cell, 0 AS step
    UNION ALL
    SELECT igeo7_parent(cell), step + 1
    FROM   hierarchy
    WHERE  igeo7_get_resolution(cell) > 0
)
SELECT step, igeo7_to_string(cell) AS readable
FROM hierarchy;
-- step 0 → '0800432'
-- step 1 → '080043'
-- step 2 → '08004'
-- step 3 → '0800'   (resolution 2)
-- ...
-- step 5 → '08'     (resolution 0)

Validate before processing

SELECT cell_id, igeo7_to_string(cell_id) AS readable
FROM   my_table
WHERE  igeo7_is_valid(cell_id);

Parquet: decode in one pass

LOAD '/absolute/path/to/build/igeo7.duckdb_extension';

SELECT
    cell_id,
    igeo7_get_base_cell(cell_id)  AS base,
    igeo7_get_resolution(cell_id) AS res,
    igeo7_to_string(cell_id)      AS readable
FROM read_parquet('data/*.parquet')
WHERE igeo7_is_valid(cell_id)
LIMIT 5;

Parquet: partition by parent resolution

COPY (
    SELECT *, igeo7_parent_at(cell_id, 3) AS parent_3
    FROM   read_parquet('data/*.parquet')
    WHERE  igeo7_is_valid(cell_id)
)
TO 'output/' (FORMAT PARQUET, PARTITION_BY (parent_3));

Stencil / kernel query (neighbourhood average)

WITH cells AS (
    SELECT cell_id, value
    FROM   read_parquet('data/*.parquet')
    WHERE  igeo7_is_valid(cell_id)
),
expanded AS (
    SELECT cell_id, value,
           unnest(igeo7_get_neighbours(cell_id)) AS neighbour_id
    FROM   cells
)
SELECT
    e.cell_id,
    igeo7_to_string(e.cell_id) AS center_readable,
    e.value                    AS center_value,
    AVG(c2.value)              AS neighbour_avg
FROM expanded e
JOIN cells c2
  ON  c2.cell_id = e.neighbour_id
 AND  igeo7_is_valid(e.neighbour_id)
GROUP BY e.cell_id, e.value;

Project structure

duckdb_igeo7/
├── CMakeLists.txt                  # Build system; appends metadata footer post-build
├── README.md                       # This file
├── src/
│   ├── igeo7_extension.cpp         # Extension entry point and all C++ functions
│   ├── auth/                       # Authalic conversion (Karney 2022)
│   │   ├── authalic.hpp/cpp        # Core math & Fourier coefficients
│   │   └── geo_auth.hpp/cpp        # WKB vertex transformer
│   └── z7/                         # Z7 C++ core library (upstream, unmodified)
│       ├── library.h
│       ├── library.cpp
│       ├── BitFieldProxy.h
│       └── util.h
├── sql/
│   └── igeo7_macros.sql            # Companion SQL macros (source after LOAD)
└── test/
    ├── test_z7.cpp                 # Standalone C++ smoke test (no DuckDB needed)
    └── test_extension.sql          # SQL smoke tests for all extension functions

Troubleshooting

Unsigned extension rejected on LOAD DuckDB v1.5.1 rejects extensions without a cryptographic signature by default. Start the CLI with -unsigned or run SET allow_unsigned_extensions = true; before loading.

ABI / entrypoint mismatch The extension must be built and run against the same DuckDB version. This extension targets v1.5.1 and uses the ExtensionLoader API and extern "C" DUCKDB_CPP_EXTENSION_ENTRY entry point introduced when ExtensionUtil was removed in PR #17772.

"Metadata invalid" on LOAD or INSTALL DuckDB v1.5.1 validates a binary metadata footer at the end of every .duckdb_extension file. This footer is appended automatically by the CMake post-build step when -DDUCKDB_DIR=... is provided. If DUCKDB_DIR is omitted the footer is skipped and loading will fail with a metadata error.

CMake finds the wrong DuckDB (e.g. a Homebrew install) Always pass an absolute path to -DDUCKDB_DIR. A relative path like ../duckdb is resolved from the CMake source directory, not the build directory, and can silently point elsewhere. Use $(pwd)/../duckdb to force shell expansion before CMake sees it.

Hex literal errors in SQL DuckDB does not support 0x hex literals. Use decimal equivalents (e.g., 15 instead of 0x0F).


Performance notes

Bit-manipulation functions (igeo7_get_resolution, igeo7_get_digit, igeo7_parent_at, igeo7_encode, etc.) are essentially free — a handful of shifts and masks per row, fully vectorised inside DuckDB's execution engine.

igeo7_get_neighbours performs GBT table lookups and carry propagation for each of 6 directions across up to 20 digit slots, all in native C++. Expect tens of millions of neighbour computations per second on a single core.

SQL macros (igeo7_decode_str, string helpers) expand to DuckDB SQL expressions at plan time. igeo7_decode_str in particular emits 20 shift/mask operations and a multi-argument CONCAT per row. For bulk inspection of millions of cells, prefer igeo7_to_string (C++) and reserve igeo7_decode_str for small result sets or debugging.


License

The Z7 core library files retain their original SPDX headers (see individual source files). The DuckDB extension wrapper (src/igeo7_extension.cpp, CMakeLists.txt, sql/) is MIT licensed.