Build a B2B prospect file from French open data — and know which contacts are wrong before you use them.
Scraping contact details is easy. The hard part is that a meaningful share of what you collect belongs to someone else, and every one of those rows is a commercial e-mail sent to a stranger under your own name.
prospect-forge collects companies from the national business register,
enriches them with contact details from OpenStreetMap, and then refuses to
hand you a contact it cannot tie to the right company.
On a real run over 1,286 construction companies around Grenoble, a naive "the company name appears on the page" filter returned these:
| Address found | What it actually was |
|---|---|
sassenage.mairie@wanadoo.fr |
The town hall of Fontaine — matched "Sassenage Étanchéité" |
avocat@delmas.fr |
A law firm, homonym of "Entreprise Delmas" |
press@novabat.com |
A global battery recycler, unrelated to the local NOVABAT SARL |
delegue.protectiondesdonnees@grandgroupe.fr |
Grand Groupe' data protection officer |
privacy@ofenbau.de |
A German stove manufacturer |
you@company.com |
An unfilled website template |
Roughly a third of the results. Each one is syntactically valid, reachable, and completely wrong.
That last one deserves a note, because it shows how these get in. The joinery
Fenêtres Most is tagged in OpenStreetMap with
website=https://www.securitas.fr/ — a contributor's mistake. Crawl that
without checking and you scrape a multinational, find its DPO, and add them to
a mailing list for local roofers. The pipeline never does anything invalid; it
is simply working on the wrong company from the first step onwards.
So the website gets verified before it is crawled, not after.
git clone https://github.com/Kerneth/prospect-forge
cd prospect-forge
pip install -r requirements.txtPython 3.10+. No API key: every source is public and free.
python -m prospect_forge \
--naf 43.91A 43.32A 43.34Z \
--postcode 38130 38100 \
--insee 38151 38185 \
-o prospects.csv| Option | Meaning |
|---|---|
--naf |
Activity codes, dotted form. 43.91A is roof carpentry. Full list |
--postcode |
Postcodes for the register lookup |
--insee |
Commune codes for the OpenStreetMap pass — find yours |
-o |
Output CSV, ;-separated with a BOM so Excel opens it correctly |
Output columns: score, name, trade, officer, address, postcode, town, created, headcount, NAF, SIREN, SIRET, phone, email, website, confidence, notes.
The notes column keeps why every contact was accepted or discarded, so any
row can be audited without re-running anything.
from prospect_forge.verify import verify_email, Confidence
verdict = verify_email("press@novabat.com", "NOVABAT SARL")
verdict.confidence # <Confidence.REVIEW: 'review'>
verdict.reason # "'press' mailbox — only large organisations run one, ..."Four ordered filters. Each is cheap, and each rejects a failure mode the others cannot see.
0 — Is this really their website?
Compares the domain to the company name before crawling anything. Catches the
securitas.fr class of error at the source.
1 — Is the domain distinctive?
plombier.com describes a trade, not a company. Whoever owns it is not the
plumber you are looking for. Trade-word domains are rejected outright.
2 — Does the address line up with this company?
Either the mailbox is on the company's own domain, or the company name appears
in the domain, or an acronym matches — "ATELIER CONSTR. MÉTALL. (ACMTS)"
against acmts.com. Legal forms and accents are folded away first.
3 — Blocklists.
Template placeholders, hosting providers, noreply@, data protection officers,
public bodies (mairie, prefecture, cci), regulated professions
(avocat, notaire, cabinet), and mailboxes only large organisations run
(press@, investor@, careers@) — a two-person roofing company has no press
office, so finding one means you found a namesake.
Anything that survives is labelled high. Anything plausible but unproven is
labelled review and kept, with the reason. Nothing ambiguous is silently
dropped, and nothing doubtful is silently promoted.
Every false positive above is a test case:
pytest -qThe suite asserts both directions — the bad addresses are caught and the genuine ones survive. A filter that rejects everything would pass half a test suite and be useless.
- It will not find many e-mail addresses. On the Grenoble construction run, 1,286 companies yielded 54 verified addresses. That is not a limitation of the tool: most small tradespeople have no website and no published e-mail. They work by phone. The phone column is the useful one on that market — and knowing that is itself a result.
- It cannot separate true homonyms. If a local company and a multinational
share a name and the mailbox looks ordinary, no textual filter can tell them
apart. Those land in
review, which is where a human belongs. - It does not use directory sites. Pages Jaunes and similar forbid automated extraction in their terms of use. Nothing here comes from them.
| Source | Licence | Used for |
|---|---|---|
| recherche-entreprises.api.gouv.fr | Open Licence 2.0 | Legal identity, NAF, headcount, officers |
| OpenStreetMap via Overpass | ODbL | Phone, website, e-mail |
Sole traders who have opted out of public distribution appear as
[NON-DIFFUSIBLE] in the register and are skipped — there is no name to contact.
Overpass instances are frequently saturated, and a busy one answers HTTP 200 with an HTML error page. The client checks the content type rather than the status code, and rotates across three public endpoints before giving up.
B2B prospecting in France is lawful without prior consent when the message concerns the recipient's professional role, the sender is identifiable, and an opt-out is provided. That is a floor, not a strategy:
- send in small batches, never the whole file at once
- include an opt-out in every message
- record an opt-out permanently, or it reappears on the next run
MIT — see LICENSE.