recherche-maison cover : the title The whole French DPE database in a browser tab, next to a search result list with a map and one certificate

The whole French DPE database in a browser tab : Parquet + DuckDB-WASM, no server

If you enjoy reading my blog and would like to support me, please consider making a donation here on paypal. Your support helps me continue creating content and allows me to make it even better. Thank you! You can also join my discord

Introduction

Did you know that the energy certificate of nearly every home sold or rented in France is open data ?

In France, before you sell or rent a home you need a DPE (diagnostic de performance énergétique), an energy certificate that gives the home a class from A (great) to G (a fridge in winter, an oven in summer). It’s not a detail : the worst homes are being pushed out of the rental market, starting with G in 2025. Every certificate is sent to ADEME, the French environment agency, and ADEME publishes all of them as open data : 15.5 million certificates for existing homes since July 2021, with 226 columns each.

A listing on Leboncoin or SeLoger only shows the class, the surface and the city. The certificate behind it has much more : the address, the real consumption, the insulation, the heating… With the class, the surface and the postcode, you can usually find it.

Why this project ? My parents are looking for a house, and my mother was using a similar tool. Then the tool closed. So I made my own : recherche-maison.com.

In this article I will explain how I serve the whole database without any database server : static Parquet files, and DuckDB running directly in your browser. You will get the architecture, the real numbers, and the one thing that surprised me about range requests. You can skip the theory part if you only want the DuckDB trick.

Quick numbers :

  • First commit on 4 September 2026
  • 20.8 million rows across four ADEME datasets (existing homes, new homes, tertiary buildings, energy audits)
  • 308 MB of search files for the whole of France, 3.0 GB with every column
  • Weekly update in 48 minutes
  • Hosting cost : close to $0 for now, inside Cloudflare’s free tiers

Features I wanted

  • Search a whole département by postcode or commune, class, surface (± a few m²), date and consumption
  • Every ADEME column kept, and checked
  • Refreshed every week
  • Cost close to zero, because it’s a side project
  • Still working with many users at the same time

Why no database server ?

You can skip this part if you’re not interested in the theory.

I looked at four options.

1. Query ADEME’s API live

  • Pros : nothing to store
  • Cons : my site would only work when ADEME’s API works. And their API sometimes answers HTTP 500 when you paginate deep.

2. A Postgres/PostGIS server

  • Pros : the classic, it works
  • Cons : about $50 a month, for a side project with zero revenue at the start

3. Cloudflare D1 (SQLite next to my Worker)

  • Pros : cheap, already in my stack for the user accounts
  • Cons : D1’s problem is not the money. It runs one query at a time, and a national query took 10 to 20 seconds in my test. So ten users searching at the same moment would wait two or three minutes.

4. Parquet files + DuckDB in the browser

  • Pros : the compute moves to the user’s computer, so it scales with users instead of against them
  • Cons : the user downloads a 36 MB engine (uncompressed) and some data, so the first search is not instant

I chose the fourth one. To understand why Parquet fits so well, a toy example. A classic database stores each certificate as one row, with its 226 values side by side. If you ask “how many D-class homes per département ?”, it must go through every row, all 226 values, to keep only 2 of them. Parquet stores the data by column : all the classes together, all the départements together. So the same question reads 2 columns and skips the 224 others.

Diagram : a row store reads every value of every row, Parquet reads only the class and département columns
The toy example : to count D-class homes per département, a row store goes through every value, Parquet only reads the two columns it needs.

Measured on the real data, this national count reads 17 MB, only 0.38% of the Parquet file. A row-oriented database like D1 has to go through all 15,512,658 rows.

The second ingredient is the HTTP range request. A Parquet file ends with a small footer that says where each piece of data is. So the browser can read the footer, then download only the bytes it needs, with a Range: bytes=... header. In theory. Keep this in mind, it comes back later.

The pipeline

Diagram : the ADEME API feeds Parquet files in a weekly GitHub Action, uploaded to an R2 bucket, served by a Worker and queried by DuckDB-WASM in the browser
The whole pipeline : a weekly job writes Parquet files to R2, and the only thing that runs SQL is your browser.

1. Fetching from ADEME

ADEME’s API has a speed limit : 500 kB/s without key, 1 MB/s with one. So I fetch with a single stream, never in parallel. A full rebuild takes 17.4 hours without key (about 247 rows per second) and around 9 hours with one. GitHub Actions stops a job after 6 hours, so the full rebuild runs on my own computer, by hand.

Then every Monday at 04:10 UTC a GitHub Action fetches only what changed. ADEME has a field for this, date_derniere_modification_dpe, and its own description literally says « À utiliser pour mettre en place une alimentation incrémentale » (use it for incremental updates). That’s about 150,000 rows a week, and the last full run took 48 minutes, 44 of them waiting for ADEME.

Deleted certificates don’t show up in this change feed. So at every run, I also compare my number of rows per département with ADEME’s count.

One funny moment : my first full ingest died after 15 hours and 3,062,035 rows, on a single value, 5.55e+181. I still don’t know what this home consumes, but it must be a lot. Lesson learned : with 15 million certificates typed by humans, plan for impossible values in every column.

2. Two Parquet files per département

The data is split into 103 partitions : the 96 départements of metropolitan France, four overseas ones, New Caledonia, one that groups the smallest overseas territories (Mayotte included), and a special one I will explain later. Each has two files :

  • search/ : only 17 columns, sorted by postcode, class and surface. For the whole of France it’s 308 MB, with a median of 2.3 MB per département and 15.2 MB for Paris.
  • dpe/ : all 226 columns plus latitude and longitude. 3.0 GB in total, 149.6 MB for Paris.

The sorting is important. Parquet cuts a file into chunks called row groups, and each chunk remembers the minimum and maximum of its columns. Since the search file is sorted by postcode, DuckDB can skip every chunk that can’t contain yours.

Everything is compressed with zstd (level 9). To compare, the same data is 10.9 GB in my local SQLite, and around 31 GB as raw CSV (estimated from the average row size).

Files are never edited in place. A new version is written next to the old one, and the manifest that points to the files is swapped last. So a user never reads a half-updated base.

3. Checking it’s really lossless

A test takes 40 certificates, rebuilds each one from the Parquet files, all 226 columns, and compares it field by field with the live record on ADEME. In my point of view “lossless” means nothing if it’s not tested, so this test is my definition of lossless. To be honest, it needs the real API, so for now I run it by hand on my computer. The weekly job doesn’t run it yet.

4. In the browser

DuckDB-WASM is loaded at the first search, not when the page opens, because the engine is heavy : 36 MB uncompressed. Funny detail, it’s too big for Cloudflare’s static assets (limit of 25 MB per file), so the engine itself is served from R2, like the data.

The Parquet files go through my Worker on /data/*, which checks that you are signed in and then reads R2. The bucket has no public URL.

A search looks like this (simplified) :

-- the listing says : class D, 62 m², Foix (09000)
SELECT *, count(*) OVER () AS total               -- total = every match, even above 50
FROM read_parquet('/data/search/dept=09/part-0000.parquet')
WHERE code_postal_ban = '09000'
  AND etiquette_dpe = 'D'
  AND surface_habitable_logement BETWEEN 60 AND 64  -- ± 2 m² tolerance
ORDER BY abs(surface_habitable_logement - 62)      -- closest surface first
LIMIT 50;

5. Checking that DuckDB really reads ranges

Remember the range requests from the theory part ? When the whole of France went online, opening one Paris certificate downloaded 152 MB. DuckDB-WASM only reads by ranges if the server answers its first HEAD with a 206, and my Worker answered 200, which HTTP allows. So DuckDB quietly downloaded every file whole, and with only one small département online before, it looked exactly like success on screen. The fix is a 206 on the Worker side, and these three flags (with only the first two, every file failed to open) :

// duck.ts
filesystem: { reliableHeadRequests: true, allowFullHTTPReads: false, forceFullHTTPReads: false },

(Firefox also answered that HEAD from its cache, so the Worker now sends it with no-store.)

And the surprise : ranges are not always faster. DuckDB-WASM sends its range requests one after the other, each one a round trip of about 150 ms, and it needs roughly one per column for each row group it touches. The Paris search touches 2 row groups of 17 columns, so it became 35 reads : 2 MB instead of 15 MB, but 6.4 s instead of about 4 s on fibre. That’s why a search file of 4 MB or less is downloaded whole, and only bigger files are read by ranges.

6. Linking to buildings and maps

For curious people only. About half of the certificates name their building in the RNB, the French national building register, and most certificates can be linked to their parcel in the cadastre, through the building or through the address. The coordinates come in Lambert-93, the French map projection, so I convert them to latitude and longitude myself, and my result matches ADEME’s own conversion almost perfectly. Except overseas, where ADEME’s point puts Réunion island in Scotland ^^. And 531,646 certificates (3.4%) were never geocoded at all, so they live in a pseudo-département called NG. That’s the special partition.

Real numbers

Measured in the browser, on the national data and my fibre connection. The times are for a new account in a fresh browser, so they include the first load of the engine. The downloaded column only counts the data :

ScenarioRequestsDownloadedTime
Search in Foix (09000), small file read whole20.76 MB3.4 s
Search in Paris 11e (75011), read by ranges352.06 MB6.4 s
One Paris certificate, all 226 columns168.1 MB4.5 s

It’s not Google-fast, I know. But it’s the whole of France, with no database server at all.

Cost

  • Storage : the whole tree, buildings and cadastre included, is about 10 GB, right at the limit of R2’s free 10 GB. Above it, R2 charges $0.015 per GB and per month, and nothing for downloads. So $0 today, and a few cents a month at worst.
  • Compute : the free Workers plan gives 100,000 requests a day. A first search in a small département costs about 5 of them, but in Paris about 38, because every range read goes through the Worker. So it’s somewhere between 2,600 and 20,000 searches a day. If it grows, the paid plan is $5 a month for 10 million requests.
  • To compare : the Postgres option was about $50 a month.

Result

recherche-maison.com works : up to 50 results sorted by closest surface, a list and a map, and the full certificate behind each one. The whole history is free. The last two months, which are the homes on sale right now, cost €5 a month. You need a Google account to search.

recherche-maison search results for Foix 09000, class E : a list of 17 certificates with their address and a map of the same homes
A search in Foix (09000) for class E : 17 certificates with their address, in a list and on the map.
recherche-maison certificate page : address, surface and date, then the certificate values like the heating need and the consumption
The certificate behind a result : address, surface and date, then every value ADEME publishes, like the heating need or the real consumption.

Honest numbers : on 15 October 2026 the site has 63 accounts and 2 paying users, and in early October Search Console gave it an average position of 30, so page 3 of Google. It’s not a success story yet, it’s a working tool looking for its users ^^

The limits, said plainly :

  • A home only appears if its certificate was filed with ADEME
  • The address is the one geocoded by ADEME, so it can be approximate
  • Several certificates can match one listing, and they are all shown

The project is also published as an official reuse of the ADEME data on data.gouv.fr, the French government’s open data portal.

Conclusion

That’s it ! In my point of view the main lesson is that a “big” open dataset doesn’t need a big server anymore. 15 million rows fit in 308 MB of Parquet, and your user’s laptop is a very good database engine. Just open the network tab once to check that your ranges really are ranges !

If you are looking for a home in France, like my parents, give recherche-maison.com a try before your next visit. And if your company needs this kind of data pipeline, I do it as a freelancer : eymeric.me. The recherche-maison project page is there too, and I explained how I built that site in my previous article.

Next step : the weekly comparison with ADEME finds some anomalies in the official data, and I want to report them to the ADEME team. It will probably be the subject of another article, stay tuned !

Resources

If you have liked this article and want to keep update on the next article and project feel free to join my discord !


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *