schema-map
See docs/reference/README.md for the MUST/SHOULD/MAY convention, and
docs/reference/shared.md for rules schema-map shares with other tools.
Inputs¶
schema-mapMUST read the input and reproject it to EPSG:4326 the same way every other tool does, viacore.io.read_and_reproject().schema-mapMUST takename_field/code_fieldstrings, each containing a{n}placeholder (e.g.adm{n}_name/adm{n}_code); these supply output naming only, never matching vocabulary. Both MUST be given together, or both omitted; if both are omitted, it MUST default toadm{n}_name/adm{n}_code.schema-mapMUST raiseValueErrorif only one ofname_field/code_fieldis given, or if either lacks a{n}placeholder.schema-mapMUST exclude any column matchingcore.constants.is_noise_column()from candidate columns entirely; they never appear in the crosswalk, not even asunmatched. A column matches if its name, case-insensitively, either exactly equals an entry incore.constants.NOISE_COLUMNS(objectid,globalid,shape_leng/shape_length/shape__length,shape_area/shape__area,ogc_fid/ogc_fid_orig/fid_orig), or equals one after stripping a trailing GDAL collision suffix (_\d+, e.g.fid_1->fid), or is exactly 10 characters long (the ESRI Shapefile DBF driver's field-name limit) with that stripped base a prefix of aNOISE_COLUMNSentry (e.g.Shape_Le_1->shape_le, a prefix ofshape_length, the DBF-truncated form of a duplicate field).
Matching¶
schema-map MUST NOT use any column name, alias, or vocabulary as a matching
signal, and MUST NOT use a value-shape assumption to decide chain
membership either; every candidate column's own values are relationally
tested against every other candidate column's values, never its name.
Value shape (_looks_code_shaped(), majority of non-null values
containing a digit) MUST only be consulted as a fallback for the code
vs. name role of a chain column once its level is already resolved. See
docs/explanation/schema_map.md and docs/adr/0064, docs/adr/0066 for the
empirical justification.
- An all-null candidate column (
COUNT(DISTINCT) = 0) MUST NOT be eligible for chain group formation: two all-null columns are trivially, vacuously bijective with each other and with nothing else, no real evidence either way, the same principle already applied to_embeds()(seedocs/adr/0069). It still MUST appear in the crosswalk asunmatched. - Every remaining candidate column, code or name alike, MUST be grouped by
identical
COUNT(DISTINCT)(constants are kept, not dropped: a single-country file's admin0 code is legitimately constant), and same-count columns clustered by pairwise verified bijection (two columns merge only if bijective with each other; a third column sharing their count but not their bijection MUST NOT prevent the other two from merging). - The admin hierarchy MUST be built as the longest path through every
coarser/finer pair of level-groups that satisfies containment
(
GROUP BY finer HAVING COUNT(DISTINCT coarser) > 1MUST return zero rows, or exactly one violating group, for every coarser/finer column pair between the two groups; see the single-violator tolerance below), not just cardinality-adjacent pairs; this lets a finer level reconnect past a level that doesn't nest cleanly (a "loose" cross-cutting attribute that happens to be embedded inside a compound code, e.g. an urban/rural classifier) instead of severing the rest of the chain. A candidate edge MUST additionally require either the coarser group'sCOUNT(DISTINCT)to be exactly 1 (a true constant, exempt from embedding), or some column in the finer group to textually contain (contains(child, parent), row for row over non-null pairs, tolerating a single violating value) some column in the coarser group, or, if no coarser/finer group pair anywhere in the file has embedding evidence at all, containment alone (seedocs/adr/0070). When multiple candidates tie for the longest path, the one with more same-level companion columns MUST win, then the one with the higher (finer)COUNT(DISTINCT). - Both the containment and embedding checks above MUST tolerate exactly
one violating value the same way a NULL already carries no evidence: a
missing-value sentinel (e.g.
"No_Pcode") reused across many real parents, never a hardcoded literal, only "exactly one distinct value explains every violation". More than one distinct violator MUST fail strictly, a genuine multi-value anomaly, not a placeholder (seedocs/adr/0071). - With
levelgiven, the finest resolved level MUST be numberedleveland each coarser one by nesting depth from it, andschema-mapMUST raiseValueErrorif that would number any resolved level below 0. Withoutlevel, a constant coarsest level MUST be numbered 0 and any other coarsest level 1, with a logged warning. - Within a resolved chain level, each column's role MUST be
codeif either it textually contains (contains(child, parent)) some column at the level's resolved parent, or it independently passes_looks_code_shaped(); otherwise it MUST bename. This check MUST run per column, never deferred to a sibling's embedding result: a column that fails to embed its parent MUST still resolve tocodeon its own value shape rather than defaulting tonamejust because another sibling in the group embedded the parent (seedocs/adr/0067). Same- role companions at one level MUST each get a numberedtarget_columnfromcode_field/name_field(the first by source-column order gets the bare rendered template, each next one the template plus an appended integer starting at 1). - Every non-code-eligible column MUST be bracketed into the chain by its
own
COUNT(DISTINCT): it lands at levelkifcode_count[k-1] < distinct_count <= code_count[k](code_count[-1]is 0); a column whose count doesn't fall into exactly one bracket MUST be left unmatched. - A bracketed column MUST additionally pass a same-level function check
against that level's code column (
GROUP BY code HAVING COUNT(DISTINCT candidate) > 1MUST return zero rows) before it's eligible for confidencenameorsupplemental; a column that fails this check entirely (neither a subset nor superset of the level) MUST be confidenceambiguous,target_columnempty. - A function-passing column MUST be confidence
supplemental,target_columnempty, when its level's chain group already has a resolvednamerole member AND its own collapse ratio (1 - COUNT(DISTINCT candidate) / level_unit_count) exceeds0.30; a bijective (exact) same-level companion cannot reach the bracket step at all, since bijection would already have merged it into the chain group at the grouping step, so a function-passing bracket candidate is always a genuine, coarser superset of the level (by pigeonhole, an onto function between equal-cardinality sets is also one-to-one, so a non-bijective function-passing candidate must be coarser) unless its collapse is within that tolerance, in which case it's treated as a same-level translation/transcription variant instead (seedocs/adr/0076). When the level's chain group has nonamerole member yet, or the candidate's collapse ratio is<= 0.30, function-passing candidates MUST instead resolve to confidencename, numberedtarget_columnfromname_field(same numbering scheme as code companions above). - A column landing in no bracket at all MUST be confidence
unmatched,target_columnandnoteboth empty. - A resolved level MUST be excluded from output (fall through to
confidence
unmatched) only when its ownCOUNT(DISTINCT)is exactly 1, a true constant; a non-constant level MUST be resolved regardless of its rank in the discovered chain, even at position 0.schema-maphas no way to tell a genuine admin0 constant from a coarsest-in-file level that merely isn't actually admin0 (a file with no country column at all) withoutlevel; it only excludes columns with no real variation to report. target_columnMUST be non-empty only for acode/namerow; everyambiguous/unmatchedrow'starget_columnMUST be empty, sinceschema-refactordrops any source column whose crosswalktarget_columnis empty (seedocs/reference/schema_refactor.md) and a human, notschema-map, decides whether to keep such a column and under what name.schema-mapMUST NOT call an LLM or any external service; matching is embedding and cardinality/containment logic only.
Outputs¶
schema-mapperforms no topology hard gate at all; it only inspects and never mutates geometry.schema-mapMUST always produce a crosswalk file, one CSV row per source column with exactly four columns:source_column,target_column,unique_count,note. Every row MUST carry aunique_count: for a row bracketed to a level (code,name,ambiguous, orsupplemental),COUNT(DISTINCT parent_code, this_column)against the level above it, catching a value reused across parents (e.g. "County 1" under two different provinces) that a same-column distinct count alone would hide; for any other row (an excluded constant level, or fullyunmatched), the column's ownCOUNT(DISTINCT). Acode/namerow'snoteMUST be empty, sincetarget_columnalready encodes the level andunique_countalready encodes the cardinality signal. A bracketedambiguousrow'snoteMUST be exactly"ambiguous, level {k}"; a bracketedsupplementalrow'snoteMUST be exactly"supplemental, superset of level {k}". Anunmatchedrow'snoteMUST be empty.- Rows MUST be ordered by resolved level descending (finest first,
matching COD-AB's own-level-then-ancestors order), name before code
within a level, then every
unmatchedcolumn last in the source file's own column order. schema-mapMUST NOT rename or drop any column in the input file itself (seedocs/reference/schema_refactor.mdfor the tool that does).
Configuration (api.schema_map.map() / CLI)¶
schema-mapMUST process exactly one input file per call.- The crosswalk path MUST default to the input path with a
_crosswalkstem suffix and a.csvextension. schema-mapMUST raiseFileExistsErrorif the output path already exists and overwriting wasn't requested.level, if given, MUST be a non-negative integer (--level).step, if given, MUST be one ofinputs,schema-map,outputs; any other value MUST raiseValueError.