All sections

Mixed H3 Resolutions

Why a set containing H3 cells from more than one resolution cannot be compared or subtracted until every cell is normalized to a single resolution.

exactstableh36 min read

Purpose

A mixed-resolution cell set is one that contains H3 cells from more than one resolution at once — a resolution-6 cell alongside a resolution-9 cell in the same array. This is a normal, expected shape for data that has been through compaction, hierarchical rollups, or merged from two sources built at different working resolutions. It is not, however, a shape that any set operation — union, intersection, difference, equality check, or membership test — can be run against correctly without normalization first. This page defines why, and states the exact normalization rule.

Why mixed-resolution sets arise

The most common source is compaction: a uniform resolution-9 cell set gets compacted for storage or transmission, and any region where all seven children of a resolution-8 parent are present gets replaced by that one parent cell — the compacted set is now, by design, a mix of resolution 8 and resolution 9 cells (and potentially coarser, if compaction cascades upward). Mixed sets also arise from merging two independently built cell sets (an inclusion built at resolution 7, an exclusion built at resolution 9), or from a rollup that stores results at whatever resolution each region's data was originally collected at.

Why comparison requires normalization

H3 cells are opaque 64-bit indices; a resolution-8 cell and a resolution-9 cell that geometrically nest inside one another do not share any bit pattern that a naive equality or set-membership check can exploit. Testing whether a resolution-9 cell is "in" a set that actually contains its resolution-8 parent requires walking the hierarchy (cellToParent upward from the resolution-9 cell, or cellToChildren downward from the resolution-8 cell) — a plain Set.has() on raw cell indices will report a false negative every time, because the parent index and the child index are different numbers. The same problem applies to union, intersection, and difference: two mixed sets cannot be unioned by simple array concatenation, because a parent cell in one set and its own children in the other set are the same geography expressed two different ways, and naively combining them produces silent double-counting rather than a correct union.

normalizeToResolution behavior

normalizeToResolution resolves every cell in a mixed set to one target resolution by expanding or folding as needed:

Coarser cell than target
Expanded to its children at the target resolution via cellToChildren. A single resolution-6 cell becomes 7 resolution-7 children, 49 resolution-8 grandchildren, and so on.
Finer cell than target
Folded up to its ancestor at the target resolution via cellToParent, then deduplicated against any other cell in the set that folds to the same ancestor.
Cell already at target resolution
Passed through unchanged.

The result is a uniform-resolution set that supports ordinary set semantics again: two normalized sets can be unioned, intersected, or diffed with plain set operations, and cell-count based area estimates become meaningful because every cell in the result has the same nominal area.

Parent-child duplicates and double counting

A parent-child duplicate is a mixed set that contains both a cell and one or more of that cell's descendants (or ancestors) — for example, a resolution-7 parent cell and one of its own resolution-9 grandchildren, both present in the same array. Left unnormalized, any per-cell aggregation (a population count per cell, a spend figure per cell) will count the overlapping ground twice: once under the parent's row, once under the child's row, because both rows describe overlapping geography under different indices. hasParentChildDuplicate detects this condition before it reaches an aggregation step, since the fix (normalize to one resolution first) is cheap but only if applied before, not after, values have already been summed.

Logical versus geometric containment

cellToChildren and cellToParent define a logical hierarchy — the H3 indexing scheme's own parent-child bookkeeping — and it is tempting to treat that hierarchy as geometrically exact: to assume a child cell's boundary lies entirely within its parent's boundary. It does not, in general. H3's grid is built on an icosahedral projection, and near cell distortion (most visibly around pentagons and face-crossing cells, but present at lower magnitude throughout the grid) a child cell's true polygon boundary can extend slightly outside its logical parent's polygon boundary. Do not use cellToParent / cellToChildren as a substitute for an actual point-in-polygon or polygon-intersection test when geometric exactness matters (for example, verifying that a fine-resolution cell physically falls inside a specific coarse-resolution polygon) — the hierarchy is exact as an indexing relationship and only approximately exact as a geometric one.

Algorithm

import {
  normalizeToResolution,
  hasParentChildDuplicate,
} from "@/lib/h3/hierarchy";

// Detect before aggregating — cheap check, expensive mistake to skip.
if (hasParentChildDuplicate(mixedCells)) {
  flagForNormalization(mixedCells);
}

// Fold coarse cells down / expand fine cells up to one target resolution.
const uniform = normalizeToResolution(mixedCells, { resolution: 9 });

The same conversion with the Python bindings (h3-py v4):

import h3

def normalize_to_resolution(cells, resolution: int) -> set[str]:
    normalized = set()
    for cell in cells:
        res = h3.get_resolution(cell)
        if res == resolution:
            normalized.add(cell)
        elif res < resolution:
            normalized.update(h3.cell_to_children(cell, resolution))
        else:
            normalized.add(h3.cell_to_parent(cell, resolution))
    return normalized

def has_parent_child_duplicate(cells) -> bool:
    by_resolution: dict[int, set[str]] = {}
    for cell in cells:
        by_resolution.setdefault(h3.get_resolution(cell), set()).add(cell)
    resolutions = sorted(by_resolution)
    for i, coarse_res in enumerate(resolutions):
        for fine_res in resolutions[i + 1:]:
            for cell in by_resolution[fine_res]:
                ancestor = h3.cell_to_parent(cell, coarse_res)
                if ancestor in by_resolution[coarse_res]:
                    return True
    return False

# Detect before aggregating — cheap check, expensive mistake to skip.
if has_parent_child_duplicate(mixed_cells):
    flag_for_normalization(mixed_cells)

# Fold coarse cells down / expand fine cells up to one target resolution.
uniform = normalize_to_resolution(mixed_cells, resolution=9)

The tested reference implementation in this knowledge base is the TypeScript in lib/; this Python walks each cell's resolution with get_resolution and expands/folds it with cell_to_children / cell_to_parent, exactly as normalizeToResolution does.

An R7 parent (amber) overlaid with R9 children (green): logical hierarchy is not exact geometric containment.
Rendered from the tested conversion code · An R7 parent (amber) overlaid with R9 children (green): logical hierarchy is not exact geometric containment.

Edge cases

Mixed resolutions show up constantly at the boundary between an inclusion built at one resolution and an exclusion built at another — normalize both to the same resolution before ever computing a set difference between them. Parent-child duplicates are the concrete symptom to test for whenever two cell sets from different pipelines are merged; a duplicate check should run as a matter of course on any merged set before it is used for area estimation, reporting, or billing.

Assumptions and limitations

Normalization assumes every cell in the mixed set is a valid H3 index at a resolution the target conversion supports; it does not repair cells that were corrupted upstream (wrong resolution recorded in metadata, cell indices from a different H3 base cell numbering scheme). It also does not restore information lost by an earlier lossy conversion — normalizing to a uniform resolution recovers a comparable set, not the original source geometry that produced the mixed set in the first place.

Edge cases affecting this page
  • - A set mixing resolutions cannot be compared or subtracted without normalization.
  • - A parent and one of its descendants both present double-count the shared area.