Marseille died in 1946 — the INSEE code-reuse trap
France's second-largest city ceased to exist on 18 October 1946. At least, that's what my database said.
I was building what sounded like a simple thing: a temporal model of French
administrative boundaries. Every commune as a series of versions — (code, name)
valid over [valid_from, valid_to) — so you can ask the map a question nobody
serves an answer to: "which commune was here, on this date?"
It matters more than it sounds. Since 2015, roughly 1,800 French communes
have disappeared into mergers (36,658 → 34,875). Any dataset keyed by INSEE
code — health, tax, elections, real estate — silently breaks when joined to today's map.
And the failure mode is vicious, because INSEE reuses codes: code
01033 meant Bellegarde-sur-Valserine until January 2019, and means
Valserhône — a different, larger territory — since. Same key, different truth,
no error message.
The data is beautiful. The semantics are a minefield.
INSEE publishes two files that make the reconstruction possible: the yearly
COG (the list of communes on January 1st) and mvtcommune — every
administrative event since 1943, with its effective date. Renames,
mergers, splits, resurrections. Each row says: on date D, unit
AV (avant) became unit AP (après), with an event type
MOD:
MOD meaning
10 renamed
20 created (carved out of existing communes)
21 re-established (a merged commune comes back)
30 abolished
31 simple merger
32 "commune nouvelle" (the post-2015 merger wave)
33 fusion-association
41 code change (chef-lieu transfer)
50 code change (department change)
My first model treated every AV row as a death and every AP
row as a birth. Elegant. Wrong in seven distinct ways — and every one of them was
discovered the same way: by counting.
The bugs, in the order the data confessed them
1. Mergers emit ghost rows. A fusion also produces rows for the
resulting communes déléguées — carrying the same code and name as the
dead commune, but with a different TYPECOM. Without filtering on
TYPECOM == COM, the delegated-commune row overwrote the real one's dates
and erased pre-merger Bellegarde entirely.
2. The absorber traverses. A merger includes an "identity row" — same code, same name on both sides — for the commune that absorbs the others. It isn't dying or being born; it's passing through. Treat it like an end+start and every absorbing commune gets chopped at every merger it survived.
3. Marseille died in 1946. The headline bug. MOD 20
(création) means a new commune was carved out of existing ones — Plan-de-Cuques,
in this case, carved out of Marseille on 1946-10-18. My model read the
AV = Marseille row as Marseille's death certificate. The source commune of a
création continues to exist. Whether an AV row ends a commune
depends on the event type — always for abolitions and mergers, never for créations.
4. Manosque was born in 1975. The mirror image: when Manosque
absorbed neighbors in a fusion-association, the cross rows pointed at Manosque —
and my model gave the thousand-year-old town a birthdate of 1975-01-01. An
AP row only starts a commune for créations, re-establishments and
commune-nouvelle formations — not for the absorbing side of a merger.
5. The dead rise. Celles (Cantal) merged away in 2016 — and was
re-established in 2025. One (code, name) pair, two disjoint validity
periods. The model must allow resurrection, or it either loses the past or corrupts the
present.
6. The commune nouvelle that keeps its name erases its own past. The subtlest one. When Osmery absorbed Lugny-Bourbonnais in 2024, the new merged commune kept the code and the name "Osmery":
MOD DATE_EFF AV AP
32 2024-01-01 18131 Lugny-Bourbonnais → 18173 Osmery
32 2024-01-01 18173 Osmery → 18173 Osmery (identity)
The cross row says "something became Osmery on 2024-01-01" — and the naive reading restarts Osmery there, deleting eighty years of history. The identity row is the tell: when present, it must cancel the same-day start. Neufchâteau, Saillans, Cussey-sur-Lison — the entire post-2015 merger wave is full of these.
7. The commune that existed for zero days. On 2018-01-01, Freigné
changed department (code 49144 → 44225) and merged
into Vallons-de-l'Erdre — the same day. Read naively, code 44225 is born in the morning
and lives forever; in truth it existed for zero days as an independent commune. Same-day
start+end with no prior history is a transition artifact: discard.
Counting as a debugging method
None of these bugs threw an exception. All of them were found the same way: INSEE publishes how many communes France has on each January 1st. So after every change, the model was asked to count.
| Model iteration | 2015 (36,658) | 2020 (34,968) | 2025 (34,875) |
|---|---|---|---|
| naive events | −178 | −136 | −131 |
| + MOD-aware semantics | −41 | −3 | +2 |
| + identity cancellation, zero-length discard | exact | exact | exact |
Then the stronger test: download all seven official yearly snapshots (2019–2025) and diff every one against the model — which is built from a single 2025 file plus the event history. Result: zero missing, zero extra, every single year. One present-day file and a complete event log reproduce seven consecutive official states of the French commune landscape. That's the property that makes event-based temporal modeling worth the pain: dates are the source of truth, snapshots become mere checksums.
What it looks like when it works

Ask the API about code 01033 and you get a different, correct answer for
every era — including the full event narrative:
curl "https://api.confinia.io/v1/communes?code=01033&at=2018-06-01"
→ Bellegarde-sur-Valserine
curl "https://api.confinia.io/v1/communes?code=01033&at=2020-06-01"
→ Valserhône (parents: 01033, 01091, 01205)
curl "https://api.confinia.io/v1/communes/01033/history"
→ Bellegarde (1943→1956) → Bellegarde-sur-Valserine (1956→2019,
absorbed Arlod & Coupy along the way) → Valserhône (2019→today)
And it generalizes: the same temporal model now holds ~165,000 historical versions across 36 European countries — Germany and the Netherlands from yearly national editions, the rest of the EU via Eurostat's LAU files (which have their own traps: whole countries silently missing from single editions — Poland's 2023 file contains 14 of its ~2,480 gminy), plus every NUTS version back to 2003, where the 2016 French région reform hides in plain sight: the 13 new régions became NUTS 1, while the 22 old ones survive as NUTS 2 for statistical continuity. Time series break on that divergence constantly.
Takeaways for anyone touching administrative data
Never trust an identifier to mean the same thing twice. Codes are
reused. A join on INSEE code without a date is a bug that hasn't fired yet.
Events beat snapshots — effective dates are truth; yearly files are
checksums. Semantics live in the event type, not in the row shape — the
same "A became B" row means death, birth, or nothing, depending on MOD.
And validate against numbers someone else published — every bug above
was invisible until a count disagreed with INSEE.