Home / Blog / When a district graduates to a region
May 11, 2026· Wine World Map
When a district graduates to a region
This is the second post in two weeks about deduping districts. The first one was about same-name within the same region — two rows for Côtes du Rhône that should be one.
This one is about same name across the region/district boundary.
When Brouilly stops being a district under Bourgogne and becomes
a region in its own right, the old district row doesn't auto-vanish.
scripts/remove-redundant-districts.js is the script that handles
those.
Why a district would graduate
Three scenarios where you'd promote a district:
- The district is famous enough to stand alone. Bolgheri is technically a coastal DOC under Tuscany, but visitors searching for Bolgheri don't think of it as "in Tuscany" — they think of Sassicaia. Promoting it to a region gives it its own landing page.
- The district has its own districts. Brouilly contains Côte de Brouilly and a handful of named climats. Districts can't have districts in our schema, so when sub-zones appear, the parent has to graduate.
- Geographic accuracy. Some appellations span far enough that treating them as a district (a point on the map) misrepresents them. Champagne is the textbook case — it's an AOC, but it's also a region with sub-zones that need their own pins.
In all three cases, the workflow is: insert a new row in regions
with the same name, leave the old row in districts for now, and
re-link any wineries that should belong to the new region.
The leftover district row is what this script cleans up.
The detection rule
The whole rule is one filter:
const redundant = ds.filter(d => {
const matchedRegionId = rByName.get(d.name.toLowerCase())
return matchedRegionId && matchedRegionId !== d.region_id
})
For each district, look up whether a region exists with the same name (case-insensitive). If yes and that region isn't the district's parent, the district is redundant.
The matchedRegionId !== d.region_id clause is there to skip the
common-and-fine case of a district whose name happens to match its
parent region (rare, but a couple of edge cases exist — Soave
shows up as both a region and a sub-district of itself in some
data sources).
The safety guard
A redundant district is only safe to delete if nothing else points
at it. The script checks wineries.district_id before any delete:
for (const d of redundant) {
const { count } = await s.from('wineries')
.select('*', { count: 'exact', head: true })
.eq('district_id', d.id)
if (count && count > 0) {
console.log(` KEEP ${d.id} ${d.name} (${count} wineries linked)`)
} else {
safe.push(d)
}
}
If even one winery still links to the district row, we keep it. The operator then has a manual cleanup job: move those wineries to the new region (or to a new district under it), and re-run.
This is the conservative choice. A more aggressive script would
re-link the wineries automatically, the way dedup-districts.js
does within a single region. We deliberately don't here because
the semantics aren't symmetric: same-name in the same region is
always a duplicate, but same-name across the region/district
boundary means a deliberate restructure is happening, and the
operator should decide what to do with the orphaned wineries.
What a typical run looks like
scanned 612 regions, 3,487 districts
17 redundant districts found
KEEP 2841 Brouilly (12 wineries linked)
KEEP 3102 Bolgheri (4 wineries linked)
15 safe to delete (no winery links)
deleted 15 redundant districts
The two kept rows in this example need attention before the next run: someone has to move those 16 wineries to the new region rows. After that the script's next pass will see zero redundant districts and exit silently.
The wider point
There are two dedup scripts because there are two kinds of duplication, and they want different policies:
| Script | Detects | Auto-relinks wineries? |
|---|---|---|
| dedup-districts.js | Same name, same region | Yes (always the older row wins) |
| remove-redundant-districts.js | District named like a region | No (operator decides) |
The temptation is to unify them. "It's all just name collisions, right?" It isn't — the two duplication patterns mean different things about the data, and asking the operator to think about them separately is part of what keeps the schema honest.
#data-model#dedup