<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Clément Igonet</title>
    <description>The latest articles on DEV Community by Clément Igonet (@clement_igonet).</description>
    <link>https://dev.to/clement_igonet</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4036304%2F89542984-83e9-4662-83bf-4dbe3542941b.png</url>
      <title>DEV Community: Clément Igonet</title>
      <link>https://dev.to/clement_igonet</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/clement_igonet"/>
    <language>en</language>
    <item>
      <title>A satellite control room from 100% open data, self-hosted in Europe</title>
      <dc:creator>Clément Igonet</dc:creator>
      <pubDate>Sun, 19 Jul 2026 10:27:09 +0000</pubDate>
      <link>https://dev.to/clement_igonet/a-satellite-control-room-from-100-open-data-self-hosted-in-europe-51eb</link>
      <guid>https://dev.to/clement_igonet/a-satellite-control-room-from-100-open-data-self-hosted-in-europe-51eb</guid>
      <description>&lt;h1&gt;
  
  
  A satellite control room from 100% open data, self-hosted in Europe
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Live demo: &lt;a href="https://overwatch.confinia.io/#57175" rel="noopener noreferrer"&gt;overwatch.confinia.io&lt;/a&gt;&lt;/strong&gt; —&lt;br&gt;
a MapLibre globe with 23 satellites moving in real time, each one clickable&lt;br&gt;
into Grafana dashboards showing battery voltages, temperatures and currents&lt;br&gt;
decoded from their actual radio beacons. Every byte of it comes from open&lt;br&gt;
data, and the whole thing runs on one rootless-podman server in Europe.&lt;/p&gt;

&lt;p&gt;This post is about the four design decisions that made it work — and one&lt;br&gt;
upstream surprise that accidentally proved the project's whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack in one diagram
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CelesTrak ─┐                ┌─ MapLibre globe (Flask) ── reads cache only
SatNOGS  ──┤─►  ingest  ─►  Postgres                        │
           (the ONLY thing           └─ Grafana ────────────┘
            touching upstream)          embedded per-satellite dashboards
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Orbits come from &lt;a href="https://celestrak.org" rel="noopener noreferrer"&gt;CelesTrak&lt;/a&gt; two-line elements,&lt;br&gt;
propagated locally with SGP4 every 15 seconds. Telemetry comes from&lt;br&gt;
&lt;a href="https://satnogs.org" rel="noopener noreferrer"&gt;SatNOGS&lt;/a&gt;, the Libre Space Foundation's global network&lt;br&gt;
of volunteer ground stations. The basemap is&lt;br&gt;
&lt;a href="https://s2maps.eu" rel="noopener noreferrer"&gt;Sentinel-2 cloudless by EOX&lt;/a&gt; — real Copernicus imagery,&lt;br&gt;
served from Europe. Postgres caches everything; the browser and Grafana read&lt;br&gt;
only from that cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: the caching boundary is the architecture
&lt;/h2&gt;

&lt;p&gt;Exactly one service is allowed to call an external API. CelesTrak firewalls&lt;br&gt;
IPs that pull more than ~100 MB/day and asks you to fetch each dataset once&lt;br&gt;
per update — a naive "fetch on every map refresh" design gets you banned, and&lt;br&gt;
deserves to be. So the ingest service fetches elements on a 6-hour cadence,&lt;br&gt;
propagates positions locally (no network involved), and polls telemetry every&lt;br&gt;
30 minutes with cursor pagination — "give me only frames newer than what I&lt;br&gt;
have." When the demo hit the front of a busy Slack channel, visitor traffic&lt;br&gt;
touched nothing upstream. Politeness and scalability turn out to be the same&lt;br&gt;
property.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: decode the radio frames yourself
&lt;/h2&gt;

&lt;p&gt;Mid-project, SatNOGS's API changed: it still serves raw frames — hex blobs&lt;br&gt;
like &lt;code&gt;60A060A0A66E60A4A660…&lt;/code&gt; — but the &lt;em&gt;decoded&lt;/em&gt; values now live only in&lt;br&gt;
their internal InfluxDB. For a day, my dashboards were empty and the fix&lt;br&gt;
options were: scrape someone else's Grafana, or decode the physics myself.&lt;/p&gt;

&lt;p&gt;The answer was &lt;a href="https://gitlab.com/librespacefoundation/satnogs/satnogs-decoders" rel="noopener noreferrer"&gt;satnogs-decoders&lt;/a&gt;:&lt;br&gt;
161 community-written &lt;a href="https://kaitai.io" rel="noopener noreferrer"&gt;Kaitai Struct&lt;/a&gt; definitions, each&lt;br&gt;
describing one satellite family's beacon layout down to the bit — "bytes&lt;br&gt;
17–18: battery voltage, big-endian, ×0.0075 volts." My ingest now runs&lt;br&gt;
&lt;code&gt;Decoder.from_bytes(bytes.fromhex(frame))&lt;/code&gt; and flattens every numeric leaf&lt;br&gt;
into Postgres rows.&lt;/p&gt;

&lt;p&gt;That outage was the best thing that happened to the project. A platform that&lt;br&gt;
depends on someone else's decoded data is a proxy; one that decodes raw&lt;br&gt;
downlinks locally is infrastructure. If you care about data sovereignty,&lt;br&gt;
the difference is everything — and it's also why a satellite with no public&lt;br&gt;
decoder (looking at you, NEMO-HD) simply can't be on my map. The honest fix&lt;br&gt;
is a &lt;code&gt;.ksy&lt;/code&gt; file, not a fake dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: normalize the chaos, keep the chaos too
&lt;/h2&gt;

&lt;p&gt;Every decoder names things differently: &lt;code&gt;csp_data_payload_eps_vbatt&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;raw_frame_rt_tlm_batt_a_v&lt;/code&gt;, &lt;code&gt;beacon_types_type_check_battery_voltage&lt;/code&gt; — and&lt;br&gt;
units vary between volts and millivolts. Dashboards written per-satellite&lt;br&gt;
don't scale past three satellites. So ingest derives canonical fields&lt;br&gt;
(&lt;code&gt;battery_v&lt;/code&gt;, &lt;code&gt;battery_i&lt;/code&gt;, &lt;code&gt;battery_pct&lt;/code&gt;, with heuristic mV/mA scaling)&lt;br&gt;
&lt;em&gt;next to&lt;/em&gt; the raw ones. One battery panel now works for the whole fleet,&lt;br&gt;
while the raw fields stay queryable for anyone who wants the real thing.&lt;br&gt;
A protocol-noise filter also drops AX.25 headers, SSIDs and checksums at&lt;br&gt;
ingest — nobody's health dashboard needs &lt;code&gt;dest_ssid_raw_hbit&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 4: fuse position and telemetry — that's the product
&lt;/h2&gt;

&lt;p&gt;Two features fall out of having orbits and telemetry in the same database,&lt;br&gt;
and they're the two I'd show a satellite operator first:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Battery vs orbital sunlight.&lt;/strong&gt; During propagation, a cylindrical&lt;br&gt;
Earth-shadow test against a low-precision solar ephemeris marks every&lt;br&gt;
position sample as sunlit or eclipsed. Overlaying that on battery voltage&lt;br&gt;
shows charge/discharge cycles tracking orbital daylight — spacecraft&lt;br&gt;
physiology, visible in one graph, from amateur radio receptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who heard whom.&lt;/strong&gt; Every SatNOGS frame names its receiving station, and&lt;br&gt;
most station names embed a Maidenhead grid locator (&lt;code&gt;KM7DOS-CN87xi&lt;/code&gt;). Decode&lt;br&gt;
the locator and you can draw the reception network on the globe: this&lt;br&gt;
satellite, heard from Kitakyushu, Buenos Aires and a garden in Bavaria, with&lt;br&gt;
lines to where the satellite was at each reception. It's a live map of the&lt;br&gt;
volunteer ground-segment — and of its gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it sovereign
&lt;/h2&gt;

&lt;p&gt;The whole stack is rootless podman on one Debian dedicated server: Postgres,&lt;br&gt;
ingest, Flask, Grafana, an OpenTelemetry collector and Prometheus for&lt;br&gt;
access observability (public dashboards stay anonymous-viewer; the ops&lt;br&gt;
folder is admin-only). A shared Caddy edge terminates TLS with per-project&lt;br&gt;
vhost files. GitHub Pages hosts a static mirror of the frontend, but every&lt;br&gt;
byte of data it shows still comes from the European server — a CDN outage&lt;br&gt;
would take down the shell, not the data; a data-sovereignty audit has&lt;br&gt;
exactly one machine to look at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;Coverage is what volunteers make it: a satellite charts only when it&lt;br&gt;
broadcasts in the clear &lt;em&gt;and&lt;/em&gt; someone's antenna heard it. Of ~1,700 alive&lt;br&gt;
satellites in the SatNOGS catalog, my weekly sweep found 23 currently&lt;br&gt;
meeting that bar — that number is the true state of open satellite&lt;br&gt;
telemetry, not a limitation of the stack. Ground tracks use a spherical&lt;br&gt;
Earth (fine for dots on a map, not for targeting). And satellites render at&lt;br&gt;
ground level for now; elevation-true rendering above the globe is next.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Multi-tenant embedding with proper short-lived-token auth (the anonymous&lt;br&gt;
viewer is a POC seam, deliberately), a small SDR ground station to stop&lt;br&gt;
depending exclusively on other people's antennas, and conversations with&lt;br&gt;
anyone who operates satellites — or any European team that wants live&lt;br&gt;
operational dashboards embedded in their product without a US hyperscaler&lt;br&gt;
in the data path. That last sentence is the actual business; the satellites&lt;br&gt;
are the proof it works.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Clément Igonet — &lt;a href="https://confinia.io" rel="noopener noreferrer"&gt;confinia.io&lt;/a&gt; ·&lt;br&gt;
&lt;a href="mailto:contact@confinia.io"&gt;contact@confinia.io&lt;/a&gt;. Data: CelesTrak, SatNOGS/Libre Space Foundation,&lt;br&gt;
Copernicus/EOX. Built with MapLibre GL, Grafana, PostgreSQL, podman.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>satellite</category>
      <category>grafana</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Marseille died in 1946 — the INSEE code-reuse trap</title>
      <dc:creator>Clément Igonet</dc:creator>
      <pubDate>Sun, 19 Jul 2026 09:06:14 +0000</pubDate>
      <link>https://dev.to/clement_igonet/marseille-died-in-1946-the-insee-code-reuse-trap-402h</link>
      <guid>https://dev.to/clement_igonet/marseille-died-in-1946-the-insee-code-reuse-trap-402h</guid>
      <description>&lt;p&gt;France's second-largest city ceased to exist on 18 October 1946. At least, that's what my database said.&lt;/p&gt;

&lt;p&gt;I was building what sounded like a simple thing: a temporal model of French administrative boundaries. Every commune as a series of versions — &lt;code&gt;(code, name)&lt;/code&gt; valid over &lt;code&gt;[valid_from, valid_to)&lt;/code&gt; — so you can ask the map a question nobody serves an answer to: &lt;em&gt;"which commune was here, on this date?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It matters more than it sounds. Since 2015, roughly &lt;strong&gt;1,800 French communes have disappeared&lt;/strong&gt; 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 &lt;strong&gt;INSEE reuses codes&lt;/strong&gt;: code &lt;code&gt;01033&lt;/code&gt; meant &lt;em&gt;Bellegarde-sur-Valserine&lt;/em&gt; until January 2019, and means &lt;em&gt;Valserhône&lt;/em&gt; — a different, larger territory — since. Same key, different truth, no error message.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data is beautiful. The semantics are a minefield.
&lt;/h2&gt;

&lt;p&gt;INSEE publishes two files that make the reconstruction possible: the yearly &lt;em&gt;COG&lt;/em&gt; (the list of communes on January 1st) and &lt;code&gt;mvtcommune&lt;/code&gt; — every administrative &lt;strong&gt;event&lt;/strong&gt; since 1943, with its effective date. Renames, mergers, splits, resurrections. Each row says: on date &lt;code&gt;D&lt;/code&gt;, unit &lt;code&gt;AV&lt;/code&gt; (avant) became unit &lt;code&gt;AP&lt;/code&gt; (après), with an event type &lt;code&gt;MOD&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first model treated every &lt;code&gt;AV&lt;/code&gt; row as a death and every &lt;code&gt;AP&lt;/code&gt; row as a birth. Elegant. Wrong in seven distinct ways — and every one of them was discovered the same way: by counting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bugs, in the order the data confessed them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Mergers emit ghost rows.&lt;/strong&gt; A fusion also produces rows for the resulting &lt;em&gt;communes déléguées&lt;/em&gt; — carrying the &lt;em&gt;same code and name&lt;/em&gt; as the dead commune, but with a different &lt;code&gt;TYPECOM&lt;/code&gt;. Without filtering on &lt;code&gt;TYPECOM == COM&lt;/code&gt;, the delegated-commune row overwrote the real one's dates and erased pre-merger Bellegarde entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The absorber traverses.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Marseille died in 1946.&lt;/strong&gt; The headline bug. &lt;code&gt;MOD 20&lt;/code&gt; (&lt;em&gt;création&lt;/em&gt;) 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 &lt;code&gt;AV = Marseille&lt;/code&gt; row as Marseille's death certificate. The source commune of a création &lt;em&gt;continues to exist&lt;/em&gt;. Whether an &lt;code&gt;AV&lt;/code&gt; row ends a commune depends on the event type — always for abolitions and mergers, never for créations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Manosque was born in 1975.&lt;/strong&gt; The mirror image: when Manosque absorbed neighbors in a fusion-association, the cross rows pointed &lt;em&gt;at&lt;/em&gt; Manosque — and my model gave the thousand-year-old town a birthdate of 1975-01-01. An &lt;code&gt;AP&lt;/code&gt; row only &lt;em&gt;starts&lt;/em&gt; a commune for créations, re-establishments and commune-nouvelle formations — not for the absorbing side of a merger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The dead rise.&lt;/strong&gt; Celles (Cantal) merged away in 2016 — and was re-established in 2025. One &lt;code&gt;(code, name)&lt;/code&gt; pair, two disjoint validity periods. The model must allow resurrection, or it either loses the past or corrupts the present.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. The commune nouvelle that keeps its name erases its own past.&lt;/strong&gt; The subtlest one. When Osmery absorbed Lugny-Bourbonnais in 2024, the new merged commune kept the code &lt;em&gt;and the name&lt;/em&gt; "Osmery":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MOD  DATE_EFF    AV                          AP
32   2024-01-01  18131 Lugny-Bourbonnais  →  18173 Osmery
32   2024-01-01  18173 Osmery             →  18173 Osmery   (identity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cross row says "something became Osmery on 2024-01-01" — and the naive reading &lt;em&gt;restarts&lt;/em&gt; Osmery there, deleting eighty years of history. The identity row is the tell: when present, it must &lt;em&gt;cancel&lt;/em&gt; the same-day start. Neufchâteau, Saillans, Cussey-sur-Lison — the entire post-2015 merger wave is full of these.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. The commune that existed for zero days.&lt;/strong&gt; On 2018-01-01, Freigné changed department (code &lt;code&gt;49144&lt;/code&gt; → &lt;code&gt;44225&lt;/code&gt;) &lt;em&gt;and&lt;/em&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting as a debugging method
&lt;/h2&gt;

&lt;p&gt;None of these bugs threw an exception. All of them were found the same way: INSEE &lt;em&gt;publishes&lt;/em&gt; how many communes France has on each January 1st. So after every change, the model was asked to count.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model iteration&lt;/th&gt;
&lt;th&gt;2015 (36,658)&lt;/th&gt;
&lt;th&gt;2020 (34,968)&lt;/th&gt;
&lt;th&gt;2025 (34,875)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;naive events&lt;/td&gt;
&lt;td&gt;−178&lt;/td&gt;
&lt;td&gt;−136&lt;/td&gt;
&lt;td&gt;−131&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ MOD-aware semantics&lt;/td&gt;
&lt;td&gt;−41&lt;/td&gt;
&lt;td&gt;−3&lt;/td&gt;
&lt;td&gt;+2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ identity cancellation, zero-length discard&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;exact&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;exact&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;exact&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then the stronger test: download all seven official yearly snapshots (2019–2025) and diff every one against the model — which is built from a &lt;em&gt;single&lt;/em&gt; 2025 file plus the event history. Result: &lt;strong&gt;zero missing, zero extra, every single year.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like when it works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0wfmon43rf4kwpluhajf.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0wfmon43rf4kwpluhajf.gif" alt="Time-slider: three communes of the Ain merging into Valserhône as the date crosses January 2019" width="759" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ask the API about code &lt;code&gt;01033&lt;/code&gt; and you get a different, correct answer for every era — including the full event narrative:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://api.confinia.io/v1/communes?code=01033&amp;amp;at=2018-06-01"&lt;/span&gt;
&lt;span class="c"&gt;# → Bellegarde-sur-Valserine&lt;/span&gt;

curl &lt;span class="s2"&gt;"https://api.confinia.io/v1/communes?code=01033&amp;amp;at=2020-06-01"&lt;/span&gt;
&lt;span class="c"&gt;# → Valserhône  (parents: 01033, 01091, 01205)&lt;/span&gt;

curl &lt;span class="s2"&gt;"https://api.confinia.io/v1/communes/01033/history"&lt;/span&gt;
&lt;span class="c"&gt;# → Bellegarde (1943→1956) → Bellegarde-sur-Valserine (1956→2019,&lt;/span&gt;
&lt;span class="c"&gt;#   absorbed Arlod &amp;amp; Coupy along the way) → Valserhône (2019→today)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it generalizes: the same temporal model now holds &lt;strong&gt;~165,000 historical versions across 36 European countries&lt;/strong&gt; — 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 &lt;em&gt;constantly&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways for anyone touching administrative data
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Never trust an identifier to mean the same thing twice.&lt;/strong&gt; Codes are reused. A join on INSEE code without a date is a bug that hasn't fired yet. &lt;strong&gt;Events beat snapshots&lt;/strong&gt; — effective dates are truth; yearly files are checksums. &lt;strong&gt;Semantics live in the event type&lt;/strong&gt;, not in the row shape — the same "A became B" row means death, birth, or nothing, depending on &lt;code&gt;MOD&lt;/code&gt;. And &lt;strong&gt;validate against numbers someone else published&lt;/strong&gt; — every bug above was invisible until a count disagreed with INSEE.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://time-slider.confinia.io" rel="noopener noreferrer"&gt;the time-slider demo&lt;/a&gt; (click any commune for its dated history) · &lt;a href="https://api.confinia.io" rel="noopener noreferrer"&gt;the API&lt;/a&gt; (free, no key during beta) · &lt;a href="https://github.com/confinia/confinia-core" rel="noopener noreferrer"&gt;the open-source core&lt;/a&gt; (Apache-2.0).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Data: INSEE COG · IGN Admin Express (Licence Ouverte 2.0, modified) · © EuroGeographics (NUTS/LAU) · © GeoBasis-DE/BKG (dl-de/by-2-0) · CBS/Kadaster (CC BY 4.0).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gis</category>
      <category>postgres</category>
      <category>opendata</category>
      <category>datascience</category>
    </item>
  </channel>
</rss>
