Data quality & freshness
Legal reference data goes stale quietly: a new interest rate is published, a court moves, a law merges two cantons. This page explains how the package keeps up, and how far you can trust each dataset.
Where each dataset comes from
| Dataset | Source | How it's built | Changes when | Review every |
|---|---|---|---|---|
| Judicial areas, arrondissements | Judicial Code, annex art. 4–5 | By hand | A law redraws the judicial map (rare; last in 2014) | 24 months |
| Cantons | Judicial Code, annex art. 1 (nl + fr) | Generated: pnpm build:cantons | A law changes the annex; municipalities merge | 12 months |
| Municipalities | Statbel open data | Generated: pnpm build:cantons | Municipalities merge (2019, 2025) | 12 months |
| Regions, provinces | Statbel REFNIS code list | Weekly: snapshots + pnpm build:data | Statbel changes its register (last in 1995) | 12 months |
| Canton, arrondissement boundaries | NGI (geo.be) | Weekly: snapshots + pnpm build:data | The NGI publishes a new version | 12 months |
| Courts | tribunaux-rechtbanken.be | Weekly: snapshots + pnpm build:data | A court moves or changes its contacts or hours | 12 months |
| Court locations | NGI (geo.be) | Weekly: snapshots + pnpm build:data | The NGI moves a point | 12 months |
| Prisons | justitie.belgium.be | Weekly: snapshots + pnpm build:data | A prison opens, closes or changes its contacts | 12 months |
| Houses of justice | vlaanderen.be, maisonsdejustice.be | Weekly: snapshots + pnpm build:data | A house moves or changes its contacts | 12 months |
| Postal codes | Statbel's conversion table | Weekly: snapshots + pnpm build:data | Statbel publishes a new table (mergers, new codes) | 12 months |
| Interest rates | FOD Financiën, Belgisch Staatsblad | By hand | Every year (legal) and every semester (commercial) | 6 months |
| Public holidays | Wet 4 januari 1974, KB 18 april 1974 | By hand | A law or decree changes the list or moves a holiday | 12 months |
"Review every" is how often a person checks the dataset against its sources, as reviewEveryMonths in datapackage.json sets it, on top of the weekly rebuild of the scraped datasets.
Generated datasets are never edited by hand. pnpm build:cantons parses the official text and refuses to write anything unless the Dutch and French versions agree and every municipality is covered; pnpm build:data refuses when a page can't be placed, a canton has no court, or a canton has no boundary in the NGI's file.
How the data is built
official site ──fetch──> snapshots (sources branch) ──parse──> data/*.json ──> pull request- Fetch. Every Monday the sources workflow fetches every page of the sources (
scripts/sources), politely: one request per second per site, with a user agent that names the project. It keeps each page on thesourcesbranch, with an index of what was fetched, when, and a hash of its content. A page is only stored again when its content changes, so the branch's history shows what changed on which day. A page that fails (a server error, a 404) keeps its last good snapshot and stops the run, which opens an issue: the data is never rebuilt from an incomplete picture. A page the source no longer links to is dropped from the snapshots; its record stays in the data, retired with avalidTo(the day before the check that missed it), so its id keeps working and a lookup for an earlier day still finds it. A page that appears gets a record with avalidFrom. - Parse.
pnpm build:datareads the snapshots (scripts/parse) and writes the courts, prisons, houses of justice and canton boundaries. It reports what it can't read, and fails when a page can't be placed. - Test. The workflow typechecks and runs the tests on the new data.
- Review. When the data changed, it opens or updates one pull request with the diff and the list of unreadable values. A person checks it against the sources and merges it.
To run it yourself: pnpm fetch:sources (writes .sources/), then pnpm build:data.
The cantons and municipalities are rebuilt by hand with pnpm build:cantons: the Belgian Official Journal's site refuses GitHub's servers (#52).
Automatic checks
On every change (CI)
- Schema validation: every data file matches its JSON Schema.
- Integrity: ids are unique and well-formed; every reference (arrondissement, canton, municipality, court, boundary) points at a record that exists; every canton has one boundary, valid GeoJSON within Belgium that adds up to the country's area without overlaps; every municipality is in exactly one canton or split over several; every canton stays within its arrondissement's provinces; every canton has exactly one justice of the peace; every division has its court's type and area; every kind of court has the number of courts the law sets; every canton falls under one court of each type (one per language in Brussels), and a court's cantons lie in its arrondissement and judicial area.
- Parsers: tested end to end on trimmed copies of real pages (
test/fixtures/sources), and each reader (phone numbers, IBANs, addresses, opening hours) on the forms seen in the sources and on what it must refuse. - Interest rates: periods are sorted, never overlap and leave no gaps; the legal rate runs per calendar year and the commercial rate per semester; the 2013–2015 contract-date rate only appears in that window.
- Contact data: every IBAN passes its ISO 13616 checksum, phone numbers are E.164 Belgian numbers, e-mail addresses are lowercase, and opening hours match the
opening_hourssyntax. - Code: strict TypeScript (including unchecked index access and exact optional properties), ESLint with type-aware rules, Prettier, and 100 % test coverage of
src/. - Packaging: the built package loads with
importand withrequire(), from every entry point, on Node 22.12, 22, 24 and 26;publintandarethetypeswrongcheck theexportsand type declarations.
Every week (scheduled)
The freshness workflow runs every Monday and opens an issue labelled data-freshness when it finds something:
- Missing rates: no legal or commercial rate for today, or no commercial rate for the next two weeks (FOD Financiën publishes it before the semester starts).
- Overdue reviews: a dataset's
lastRevieweddate indatapackage.jsonis older than itsreviewEveryMonths. - Dead sources: an official source URL in
datapackage.jsonno longer responds.
Run them yourself with pnpm test:freshness (add FRESHNESS_ONLINE=1 to check the URLs).
How to read a value
- Absent is not zero.
getInterestRatereturnsnullfor a day without a rate on file, rather than guessing; a field the source doesn't give (a court's address, machine-readable opening hours) is missing rather than filled in. Handle both. - Too early is an error. A lookup for a day before a dataset's coverage throws a
RangeError: the data can't say, and today's answer would look like the right one. - Contact data is the least stable. Phone numbers, e-mail addresses and opening hours change without notice. Every record says which pages it was read from and when (
sources); check the court's page (website) when it matters. - Legal text wins. For split municipalities, the canton's
territoryis the law's own text, verbatim. When a summary, or the NGI's boundary, and the text disagree, the text is right.
Reporting a mistake
Open a data correction with the record id and a link to the official source. Corrections are released as patch versions.