← confinia.io

Marseille died in 1946 — the INSEE code-reuse trap

Confinia · July 2026 · the war stories behind an open-source temporal boundary API

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 4914444225) 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 iteration2015 (36,658)2020 (34,968)2025 (34,875)
naive events−178−136−131
+ MOD-aware semantics−41−3+2
+ identity cancellation, zero-length discardexactexactexact

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

Time-slider animation: three communes of the Ain merging into Valserhône as the date crosses January 2019

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.