Skip to content

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 ​

DatasetSourceHow it's builtChanges whenReview every
Judicial areas, arrondissementsJudicial Code, annex art. 4–5By handA law redraws the judicial map (rare; last in 2014)24 months
CantonsJudicial Code, annex art. 1 (nl + fr)Generated: pnpm build:cantonsA law changes the annex; municipalities merge12 months
MunicipalitiesStatbel open dataGenerated: pnpm build:cantonsMunicipalities merge (2019, 2025)12 months
Regions, provincesStatbel REFNIS code listWeekly: snapshots + pnpm build:dataStatbel changes its register (last in 1995)12 months
Canton, arrondissement boundariesNGI (geo.be)Weekly: snapshots + pnpm build:dataThe NGI publishes a new version12 months
Courtstribunaux-rechtbanken.beWeekly: snapshots + pnpm build:dataA court moves or changes its contacts or hours12 months
Court locationsNGI (geo.be)Weekly: snapshots + pnpm build:dataThe NGI moves a point12 months
Prisonsjustitie.belgium.beWeekly: snapshots + pnpm build:dataA prison opens, closes or changes its contacts12 months
Houses of justicevlaanderen.be, maisonsdejustice.beWeekly: snapshots + pnpm build:dataA house moves or changes its contacts12 months
Postal codesStatbel's conversion tableWeekly: snapshots + pnpm build:dataStatbel publishes a new table (mergers, new codes)12 months
Interest ratesFOD Financiën, Belgisch StaatsbladBy handEvery year (legal) and every semester (commercial)6 months
Public holidaysWet 4 januari 1974, KB 18 april 1974By handA law or decree changes the list or moves a holiday12 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
  1. 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 the sources branch, 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 a validTo (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 a validFrom.
  2. Parse. pnpm build:data reads 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.
  3. Test. The workflow typechecks and runs the tests on the new data.
  4. 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_hours syntax.
  • 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 import and with require(), from every entry point, on Node 22.12, 22, 24 and 26; publint and arethetypeswrong check the exports and 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 lastReviewed date in datapackage.json is older than its reviewEveryMonths.
  • Dead sources: an official source URL in datapackage.json no 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. getInterestRate returns null for 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 territory is 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.

Code under MIT, data under CC BY 4.0. Reference data, not legal advice: check the official sources.