edge-mosaic
edge-mosaic fits a child layer that is already the finished output of a
prior edge_extend() run into a new/different parent/clip layer, without
re-running Voronoi extension. It exists because edge-match redundantly redoes
extension for children that were already extended by an earlier pipeline
run: edge-match's own Colombia-scale profiling (see docs/explanation/edge_match.md)
shows the groups stage (where per-parent extension happens) at ~85% of
total wall time. If the geometry is already extended, only assignment,
clipping, and seam-closing need to happen: the same three primitives
assign, edge-clip, and edge-stitch expose standalone (see their own explanation
docs); edge-mosaic is a thin wrapper chaining assign-one → edge-clip → edge-stitch.
Pipeline¶
_01_inputs: loads both layers raw viacore.io.read_and_reproject, once per child file plus once for the parent ({name}_child_01,{name}_parent_01). Neither side is coverage-checked or -cleaned; see "Why neither input is coverage pre-checked" below.- assign: calls
core.assign.assign_one()directly, the same per-file majority-vote pairingassign-oneexposes standalone, seedocs/explanation/assign.md. Also narrows{name}_parent_01down to only the parent fids any child was actually assigned to, before clip runs. _01_clip: a thin wrapper joiningparent_fidonto{name}_child_01from{name}_02_assign, then callingcore.edge_clip.main(), the same per-parent_fid-subprocess, adaptively grid-tiled clipedge-clipexposes standalone, seedocs/explanation/edge_clip.md. Whenmergeis set (see "Parent gap-fill and child passthrough" below), this stage alsoUNION ALL BY NAMEs every whole unmatched child file's own already-extended geometry into{name}_03immediately after the normal clip; the api layer then calls the sharedcore.assign.fill_unmatched_parents()to append every zero-children parent's own geometry into the same table, before stitch ever runs._02_stitch: callscore.edge_stitch._02_clean.main()directly, the same whole-tableST_CoverageCleanpassedge-stitchexposes standalone, seedocs/explanation/edge_stitch.md. Passthrough/gap-fill rows (if any) are already part of{name}_03by this point, so they get the same chance to resolve seams against their clipped neighbors as everything else._03_outputs: the samecheck_valid_topology()hard gate asedge-match, using its defaultgap_maximum_width=SNAP_TOLERANCEfor the same parent-layer-hole reason (seedocs/explanation/edge_match.md's "check_valid_topologyand parent-layer gaps",docs/adr/0035,docs/adr/0039), an issues report listing unassigned/passthrough children (no dropped-group kind, since there are no groups), any gap-filled parents, plus any leftover gap wider thanSNAP_TOLERANCE, a warning log if any such gap remains, and export (only when the issues report has rows).
Multi-file children¶
Unlike every other tool here, the child role MAY span multiple files in one
call (the portolan catalog stores one extended.parquet per country, never
pre-combined); the parent/clip layer stays single-file. output_path MUST
be given explicitly whenever multiple paths are passed, since there's no
single filename to default one from, and step MUST be None (see
docs/adr/0079).
With more than one input path, _mosaic_multi_file() (api/edge_mosaic.py)
runs inputs+assign+clip in a per-file loop instead of combining every
file into one table up front, the same memory-safe pattern standalone
edge-clip's own multi-file mode used before it was reverted to a strict
1:1 primitive (ADR-0023, ADR-0024, ADR-0080): the parent is loaded once
into a pristine snapshot and its heavy-part tile decomposition cached
(core.assign.prepare_parent_tiles()), then each children file is loaded,
assigned, and clipped alone against a fresh copy of that snapshot, one at a
time. Each file's clip result is folded into a running {name}_03}
accumulator immediately (UNION ALL BY NAME) rather than held until a
final combine, keeping peak memory to roughly one parent plus one children
file at a time, not one parent plus every children file. fid is kept
globally unique across the whole run via a running offset applied right
after each file loads (not by the union-time row_number() a
single-file/combined-table run uses), then reassigned fresh via
ROW_NUMBER() over the fully accumulated result once the loop ends,
preserving the same "fid is renumbered fresh after the union" contract a
combined-table run also guarantees. stitch/outputs still run exactly
once, over the fully accumulated result, since closing a seam between two
files' clipped output needs both sides' geometry at once (see ADR-0079).
Every row on the internal {name}_04 table still carries a source_file
column tagging its origin file, whichever assembly path produced it; it's
an assign-one working column, stripped before the exported output (see
docs/adr/0087).
Parent gap-fill and child passthrough¶
Both opt-in via the single boolean merge flag (CLI: --merge), off by
default; --parent-include/--parent-exclude/--child-include/
--child-exclude/--prefer further narrow which columns survive (see
docs/explanation/assign.md).
Child passthrough. A whole child file with no overlap with any parent
(a country genuinely missing from the parent/clip layer) is dropped by
default; with merge set, that file's own already-extended geometry is
kept in the output unclipped instead, reported as a kind='passthrough'
issues row rather than unassigned. Scope is whole-file only: an
individual child dropped from an otherwise-matched file (core.assign's
per-file majority vote already decided it doesn't belong there) is
unaffected either way. _01_clip.py's main() builds this directly as a
UNION ALL BY NAME selecting every child row whose fid is in
{name}_02_unassigned, immediately after the normal clip.
Parent gap-fill. A parent matched by zero children is dropped by
default; with merge set, that parent's own geometry and carried columns
are kept in the output unclipped instead, reported as a kind='gap-fill'
row. This is the shared core.assign.fill_unmatched_parents() helper
(relocated from a mosaic-local fill_gaps() this session so edge-match
can call it too, see docs/adr/0088), called from the api layer right
after _01_clip.main() returns, against a {name}_parent_full snapshot
taken before assign narrows {name}_parent_01 to only-matched fids.
Both mechanisms are identical in outcome to edge-match's own merge,
given an equivalent already-extended/raw child set against the same
parent (see docs/explanation/edge_match.md). edge-clip has no
equivalent option (it stays a strict 1:1 primitive with no drop/keep
decision to make). merge always couples attribute carry-forward with
both passthrough mechanisms; there is no way to get one without the
other (see docs/adr/0079, docs/adr/0088).
Why neither input is coverage pre-checked¶
Neither the parent nor the child layer is checked or cleaned for coverage
violations before assign/clip runs (see ADR-0018). _03_outputs.py's hard
check_valid_topology() gate on the final stitched output already
guarantees correctness regardless: there is no path where a dirty parent
or child silently reaches export undetected, only a loud failure. The
child layer is additionally assumed to already be a finished edge_extend()
output, which edge-mosaic never re-verifies.
edge-match vs. edge-mosaic¶
edge-match |
edge-mosaic |
|
|---|---|---|
| Input assumption | Child layer is raw, unextended | Child layer is already a finished edge_extend() output |
| Assign strategy | assign-many (per-child plurality) |
assign-one (per-file majority vote) |
| Cost driver | Per-group Voronoi extension (~85% of wall time) | Assign + clip + stitch only, no extension |
| Isolation | Two subprocess generations: per-group edge-extend, then batched per-parent_fid edge-clip (docs/adr/0020) |
Per-parent_fid subprocess only, boundary adaptively grid-tiled (docs/adr/0015, docs/adr/0016, docs/adr/0017) |
| When to use | Child layer hasn't been extended yet | Reusing pre-extended layers against a new/different parent |
Caveats¶
Assign runs against overshoot geometry. See docs/explanation/assign.md's
Caveats section for the full detail on assign-one's per-file majority vote
and its residual risk (a file too small to form a real majority).
Clip runs one parent fid at a time, each in its own subprocess with its
boundary adaptively grid-tiled. See docs/explanation/edge_clip.md for the
full detail.
Cross-provenance seam risk. docs/explanation/edge_stitch.md documents
genuine (non-float-noise) seam disagreements up to ~645m between two tiles
computed independently. edge-mosaic's children can come from wholly different
tool versions or vintages (the portolan catalog has per-country pipeline
drift, e.g. phl has v01 through v03), with no guarantee that any two
extended.parquet files being combined into one edge_mosaic() input were even
produced by compatible edge_extend() versions (only empirically-confirmed
schema/CRS compatibility). The check_valid_topology() hard gate
still runs and raises before export, but a multi-provenance mosaic run
should have its parent-parent boundaries spot-checked visually (e.g. via
the geo-preview skill), not just trusted because the hard gate passed.
Use --debug to inspect the {name}_04 table's source_file column
(dropped from the exported output, see docs/adr/0087) to find which two
files actually meet at a flagged seam.
Unclipped geometry meeting clipped neighbors. With --merge set, a
passthrough file's boundary was never intersected against the parent layer,
unlike every clipped neighbor
around it; any seam disagreement there is on top of the ordinary
cross-provenance risk above, not instead of it. Stitch gets a chance to
resolve it like any other seam, and the hard gate still raises if it
can't, but a passthrough run is worth the same visual spot-check as a
multi-provenance one.
Opt-in schema-fill composition (fill_schema)¶
edge-mosaic MAY invoke schema-fill's own fill logic itself, right
after stitching and before export, via fill_schema=True (CLI:
--fill-schema). This lives entirely in api/edge_mosaic.py, at both
insertion points (the single-file step loop's outputs branch and
_mosaic_multi_file()'s own final stage), calling
core.schema_fill._02_fill.main() directly through the private
api._schema_fill_compose helper; core.edge_mosaic itself is
unchanged and still MUST NOT depend on core.schema_fill/
core.schema_map (see docs/reference/shared.md, docs/adr/0095).
fill_schema and merge are conceptually complementary but
independently gated flags, not aliases: merge's own
fill_unmatched_parents() (docs/adr/0083) fills a geometry-coverage
gap, a parent with zero matched children, by keeping its own unclipped
geometry in the output; fill_schema fills a schema-depth gap, a row
whose admin-hierarchy columns don't reach as deep as some other row's,
by cascading each column family down to the row's own real depth. Both
can be set together freely; neither implies the other.