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.

Concentrated reassignment from a real boundary change compared against diffuse tolerance churn Two maps of the same area are shown. In the first, reassigned features form a dense contiguous band along one internal boundary that has moved, with the rest of the area unchanged; this is labelled a real boundary change. In the second, reassigned features are scattered thinly along every internal boundary across the whole area; this is labelled tolerance churn caused by a coarser generalisation. Both maps report the same total reassignment count, stated beneath them, and a note says the count alone cannot distinguish the two. Same count, opposite diagnoses — only the spatial pattern separates them real boundary change 1 842 features reassigned all along one moved boundary tolerance churn 1 842 features reassigned thinly along every boundary Concentration is the diagnostic: cluster the reassigned features by which boundary they sit near.

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.
Confusion matrices distinguishing a directional boundary move from reciprocal churn Two three-by-three matrices of old district against new district are shown. In the first, a single off-diagonal cell holds a large count for the pair A to B while its reciprocal B to A is near zero, which indicates a directional boundary move. In the second, the pairs A to B and B to A hold similar moderate counts, as do B to C and C to B, which indicates reciprocal tolerance churn rather than displacement. A note states that reciprocity is the discriminator. Reciprocity separates a move from noise directional — a real move to A to B to C from A from B from C 1 781 3 149 211 one dominant ordered pair reciprocal — tolerance churn to A to B to C from A from B from C 318271 302289 274288 every pair mirrored at similar magnitude Totals are within a few percent of each other; the count alone would have told you nothing. Reassignment concentration: share of changes on the top edges Two refreshes are compared by how concentrated their reassignments are. In the intended refresh, the top edge accounts for most changes and the next three account for almost all the rest, matching the published boundary change. In the unintended re-generalisation, no single edge accounts for more than a small share and the changes are spread across dozens of edges. The note states that concentration is the diagnostic, not the total count. Concentration, not count, distinguishes an intended change from churn intended refresh · top edge 84% of changes intended refresh · next three edges 13% intended refresh · all others 3% re-generalisation · top edge 7% — no dominant edge re-generalisation · next three 16% re-generalisation · all others 77% spread thin

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.