Detecting Reassignment After a Boundary Refresh
When an administrative boundary layer is refreshed, some features change district. That is the point of the refresh. What matters operationally is which ones and why: a few thousand addresses moving because a ward was genuinely redrawn is a correct result to communicate downstream, while the same number moving diffusely across the whole country because the new boundaries were generalised at a coarser tolerance is a data-quality regression wearing the same clothes. Both produce identical match rates, identical row counts, and identical validity checks. This guide covers how to measure reassignment between runs, how to separate a real boundary change from tolerance churn by looking at where the changes fall, and how to publish the result so downstream consumers can react. It belongs to spatial join and enrichment quality checks under spatial data freshness and quality metrics.
Problem framing: reassignment is expected, its shape is not
Every boundary refresh produces reassignments, so the count on its own carries little information. Three questions turn it into a diagnosis.
Are the reassignments concentrated? A real boundary move affects features along that boundary and nowhere else. If reassignments cluster tightly against one or two internal edges, the refresh did what it was supposed to. If they are spread thinly along every edge in the layer, the geometry changed everywhere by a small amount — which is what a re-generalisation, a precision change, or a reprojection produces.
How far inside were they before? Features that were already sitting within the combined positional tolerance of the two layers were always going to flip; features that were comfortably inside a polygon and have now moved districts indicate a genuine geometric change. Comparing the previous boundary margin of the reassigned set against the population is the sharpest single discriminator available.
Do the reassignments reciprocate? A real boundary move shifts features predominantly in one direction — from district A to district B along the moved edge. Tolerance churn moves features both ways across the same edge, because it is noise rather than displacement. A confusion matrix of old district against new district shows this immediately.
Implementation: compare assignments between runs
Retain the previous assignment and diff. The comparison is cheap and the retention cost is one identifier column per feature.
-- Reassignment diff between the previous accepted run and the current one.
CREATE TABLE enrichment.reassignment AS
SELECT
cur.address_id,
cur.geom,
prev.district_id AS old_district,
cur.district_id AS new_district,
prev.margin_m AS old_margin_m,
cur.margin_m AS new_margin_m,
-- Which internal edge is this feature nearest to? The grouping key that
-- turns a count into a spatial pattern.
(SELECT b.edge_id
FROM curated.district_edges b
ORDER BY cur.geom <-> b.geom
LIMIT 1) AS nearest_edge_id
FROM enrichment.addresses_enriched cur
JOIN enrichment.addresses_enriched_prev prev USING (address_id)
WHERE cur.district_id IS DISTINCT FROM prev.district_id;
-- Concentration: how much of the reassignment sits on the top few edges?
SELECT nearest_edge_id,
COUNT(*) AS features,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct_of_total
FROM enrichment.reassignment
GROUP BY 1
ORDER BY 2 DESC
LIMIT 10;
A concentrated change puts most of its mass on one or two edges — commonly eighty percent or more on a single edge. Diffuse churn spreads across dozens with no edge exceeding a few percent. That single query answers the first question outright.
The margin comparison answers the second.
-- Were the reassigned features already living on the boundary?
SELECT
ROUND(AVG(old_margin_m)::numeric, 2) AS mean_prior_margin_m,
COUNT(*) FILTER (WHERE old_margin_m < 1.0) AS was_within_tolerance,
COUNT(*) AS reassigned
FROM enrichment.reassignment;
-- A high was_within_tolerance fraction means the join was already fragile:
-- these features would have flipped on any regeneration, refresh or not.
And the direction question is a confusion matrix.
SELECT old_district, new_district, COUNT(*) AS features
FROM enrichment.reassignment
GROUP BY 1, 2
HAVING COUNT(*) > 10
ORDER BY 3 DESC;
-- Real move: one dominant ordered pair, few or no reciprocals.
-- Tolerance churn: pairs appear in both directions with similar counts.
Verification: rehearse the refresh before accepting it
Run the diff against the candidate boundary layer before promoting it, in a staging schema, and require three facts before the refresh is accepted.
The concentration must match the published change. If the boundary authority announced one ward redrawn and the diff spreads across forty edges, the layer contains changes nobody documented — usually a re-generalisation shipped alongside the intended edit.
The reciprocity must be directional for the intended edges. Mirrored counts on the edges that were supposed to move mean the geometry moved by less than the positional tolerance, which is not a boundary change at all.
The prior-margin distribution must not be dominated by tolerance cases. If most reassigned features were already within a metre of the old edge, the join is riding the noise floor and the refresh is merely reshuffling it — the fragility measurement described in the parent topic, which wants fixing at source rather than accepting.
Gotchas
Diffing against the wrong baseline. Comparing against the last attempted run rather than the last accepted one folds a failed load’s partial assignments into the diff. Keep an explicit pointer to the last accepted output.
No stable feature identifier. Without a persistent left-feature identifier there is nothing to join on and reassignment is unmeasurable. If the left layer regenerates identifiers per run, the enrichment pipeline needs to establish a stable key before any of this works.
Treating reassignment as an error. It is a fact to be characterised and communicated, not a failure to be suppressed. The response to a well-diagnosed real boundary move is to publish it downstream, not to block the refresh.
Comparing across a projection change. If the left layer’s projection changed between runs, margins are incomparable and every feature looks reassigned. Assert projection stability first, using the checks in coordinate reference system validation.
Silent downstream propagation. Consumers aggregating by district will see totals shift with no explanation unless told. Publish the reassignment summary alongside the layer status so a downstream job can decide whether its historical comparison is still valid.
FAQ
How large a reassignment should trigger review?
Any reassignment at all should be characterised; the question is what triggers a block. A useful rule is to block promotion when reassignment exceeds a fraction of the layer — a tenth of a percent is a reasonable starting point — and the pattern is reciprocal rather than directional, since that combination indicates churn rather than an intended change.
Should the previous assignment be retained indefinitely?
Retain the last accepted assignment plus a periodic snapshot — monthly is usually enough. The last accepted run supports the diff; the older snapshots support answering “when did this address change district”, which arrives as a question from a consumer sooner or later.
How does this interact with the exposure calculation after an incident?
Directly. If a bad boundary layer was promoted and later rolled back, the reassignment diff identifies exactly which features carried a wrong district during the window, which is the population figure that calculating data exposure windows after a bad load needs.
What if the boundary authority publishes no change notes?
Then the diff is the change note, and it is worth writing one from it: which edges moved, how many features moved, in which direction. That document is more useful than anything the authority would have supplied, because it is expressed in terms of your own data.
Should reassignment be tracked for proximity joins too?
Yes, and it is often more volatile there. A nearest-neighbour assignment flips whenever a slightly closer candidate appears or disappears, so a routine update to the right layer can reassign a large share of the output without any boundary having moved at all. The same diff applies; the grouping key becomes the matched candidate rather than the nearest edge, and the discriminator becomes whether the new match is meaningfully closer or merely closer by centimetres.
Can this detect a bad refresh before it is applied?
That is the intended use. Run the diff against the candidate layer in staging, apply the three acceptance facts above, and promote only on a pass. Catching a re-generalisation before promotion costs one staging run; catching it afterwards costs a rollback and an exposure calculation.
Keep the reassignment summary itself as a small published artefact rather than a one-off query result. Downstream teams comparing this quarter against last will need it, and reconstructing it later requires both boundary versions to still be available.
Related
- Spatial join and enrichment quality checks — the parent topic covering the full join metric set.
- Monitoring point-in-polygon match rates — the match-rate signal that a refresh also moves.
- Detecting attribute drift in slowly changing layers — the attribute-side analogue of a boundary refresh diff.