diff --git a/scripts/04_build_maps.py b/scripts/04_build_maps.py
index 38737ee..71ccacb 100644
--- a/scripts/04_build_maps.py
+++ b/scripts/04_build_maps.py
@@ -80,9 +80,9 @@
from _lib.be.region import derive_region as derive_be_region
from _lib.bg.geometry import BGPolygonIndex
from _lib.bg.region import derive_region as derive_bg_region
+from _lib.ch.canton import canton_name as ch_canton_name
from _lib.ch.geometry import CHCommuneIndex, GESitgIndex
from _lib.ch.geometry import resolve as ch_resolve_geometry
-from _lib.ch.canton import canton_name as ch_canton_name
from _lib.ch.region import derive_region as derive_ch_region
from _lib.cy.geometry import CYPolygonIndex
from _lib.cy.region import derive_region as derive_cy_region
diff --git a/scripts/_lib/assets/app.js b/scripts/_lib/assets/app.js
index 4f341b3..6113f44 100644
--- a/scripts/_lib/assets/app.js
+++ b/scripts/_lib/assets/app.js
@@ -866,6 +866,24 @@
return 'indeterminate';
}
+ // A region's slug list mixes two legal tiers: appellations proper and their
+ // complementary geographic designations (FR DGC / premier-cru climat, ES
+ // subzona, IT sottozona, DE Einzellage). One combined number implies a legal
+ // precision it doesn't have — Burgundy reads 869 where the regulator
+ // recognises 84 appellations carrying 785 complementary designations.
+ // Written by both the initial paint and the filter-driven refresh, so it
+ // lives here rather than being duplicated at either call site.
+ function renderRegionCount(el, parents, subs) {
+ if (!el) return;
+ el.innerHTML = subs
+ ? `${parents}+${subs}`
+ : `${parents}`;
+ el.setAttribute('title', fmt(
+ subs ? LABELS.region_count_title : LABELS.region_count_title_flat,
+ { p: parents, s: subs },
+ ));
+ }
+
function buildAppellationFacet() {
const el = document.getElementById('facet-appellations');
if (!el) return; // defensive null-guard
@@ -882,15 +900,21 @@
const openLbl = escapeAttr(fmt(LABELS.open_appellation_aria, { name: rec.name || slug }));
return ``;
}).join('');
+ let parentCount = 0;
+ for (const s of slugs) if (!AOCS[s].is_sub_denomination) parentCount++;
+ const subCount = slugs.length - parentCount;
const safeRegion = escapeAttr(region);
// Checkbox lives outside `` (sibling of ``,
// not a descendant) so the nested-interactive-in-summary
// accessibility warning doesn't fire. Visual layout is restored
// via `.region-group-wrap`'s flex rule — checkbox + disclosure
// sit in the same row.
- html.push(`
${escapeHtml(label)}${slugs.length}
${items}
`);
+ html.push(`
${escapeHtml(label)}
${items}
`);
}
el.innerHTML = html.join('');
+ el.querySelectorAll('.region-group > summary > .count').forEach(c => {
+ renderRegionCount(c, +c.dataset.parents, +c.dataset.subs);
+ });
// Reapply current search visibility (so a tree rebuild during a typed
// query keeps the filtered view).
refreshFacetVisibility('facet-appellations', filters.q);
@@ -1349,17 +1373,23 @@
const except = new Set(['appellations']);
appEl.querySelectorAll('.region-group').forEach(group => {
let visible = 0;
+ let visibleParents = 0;
group.querySelectorAll('label').forEach(lbl => {
const inp = lbl.querySelector('input[type=checkbox]'); if (!inp) return;
const slug = inp.dataset.key; const rec = AOCS[slug];
const reachable = rec ? matchesExceptFacets(rec, slug, except) : false;
const hide = !reachable && !inp.checked;
lbl.classList.toggle('facet-unavailable', hide);
- if (!hide) visible++;
+ if (!hide) {
+ visible++;
+ if (!(rec && rec.is_sub_denomination)) visibleParents++;
+ }
});
(group.parentElement || group).classList.toggle('facet-unavailable', visible === 0);
- const cs = group.querySelector(':scope > summary > .count');
- if (cs) cs.textContent = String(visible);
+ renderRegionCount(
+ group.querySelector(':scope > summary > .count'),
+ visibleParents, visible - visibleParents,
+ );
});
}
}
diff --git a/scripts/_lib/fr_wine_region.py b/scripts/_lib/fr_wine_region.py
index 96a9ae3..81f2369 100644
--- a/scripts/_lib/fr_wine_region.py
+++ b/scripts/_lib/fr_wine_region.py
@@ -56,6 +56,19 @@
"roussette-du-bugey",
)
+# Two BOURGOGNE-bassin appellations lie outside Burgundy proper. INAO's
+# comité régional is an administrative grouping, not a wine region:
+# Côtes du Forez sits in the upper Loire beside Côte roannaise and Côtes
+# d'Auvergne (which INAO itself files under VAL DE LOIRE), and Coteaux du
+# Lyonnais continues the Beaujolais granite south of Lyon.
+_LOIRE_SLUGS: frozenset[str] = frozenset({
+ "cotes-du-forez",
+})
+
+_LYONNAIS_SLUGS: frozenset[str] = frozenset({
+ "coteaux-du-lyonnais",
+})
+
def derive_wine_region(record: dict[str, Any]) -> str:
"""Return the wine-region bucket for an FR record.
@@ -69,6 +82,10 @@ def derive_wine_region(record: dict[str, Any]) -> str:
return bassin
slug = record.get("slug") or ""
target = record.get("parent_slug") or slug
+ if target in _LOIRE_SLUGS:
+ return "VAL DE LOIRE"
+ if target in _LYONNAIS_SLUGS:
+ return "BEAUJOLAIS"
if target in _JURA_SLUGS:
return "JURA"
if target.startswith(_BUGEY_PREFIXES):
diff --git a/scripts/_lib/map_template.py b/scripts/_lib/map_template.py
index 812acf3..e3447ad 100644
--- a/scripts/_lib/map_template.py
+++ b/scripts/_lib/map_template.py
@@ -54,6 +54,10 @@ def build_labels(_: Callable[[str], str]) -> dict[str, str]:
"options_h": _("Options"),
"active_filters_aria": _("Filtres actifs"),
"select_all_aria": _("Tout sélectionner"),
+ "region_count_title": _(
+ "{p} appellations · {s} dénominations géographiques complémentaires"
+ ),
+ "region_count_title_flat": _("{p} appellations"),
"open_appellation_aria": _("Ouvrir la fiche de {name}"),
"open_appellation_title": _("Ouvrir la fiche"),
"show_spirits_label": _("Inclure les spiritueux"),
@@ -1310,6 +1314,9 @@ def _country_name(cc: str) -> str:
"styles", "styles_simple", "classifications",
"grapes_principal", "grapes_accessory", "grapes_all",
"bbox", "bbox_villages", "geom_source",
+ # buildAppellationFacet splits each region's count into appellations
+ # vs complementary geographic designations.
+ "is_sub_denomination",
})
@@ -1953,6 +1960,7 @@ def _emit(slug: str, meta: dict, ssr: str, has_card: bool) -> None:
underline on name-hover signals the "go to this one" link affordance. */
.facet label[data-slug] > .name:hover {{ text-decoration:underline; text-decoration-color:#a8c08a; text-underline-offset:2px }}
.facet .count {{ color:#666; font-size:11px; margin-left:4px }}
+ .facet .count .sub-count {{ opacity:0.65; margin-left:3px }}
.facet .syns {{ color:#888; font-size:11px; font-weight:normal }}
/* Keyboard/SR path to open an appellation panel (the WebGL polygons aren't
DOM-reachable). Subtle until the row is hovered or the button is focused. */