The problem
In Italy, every company is required by law1 to organize its first aid according to its group: A, B or C. The group decides how long the mandatory course is, how often it must be refreshed, and what first-aid kit is required. And it depends on two things: the number of workers, and the company’s risk level, measured by INAIL, the national workplace insurance institute.
The catch is that companies know their ATECO code (the official classification of economic activities), not their INAIL risk group. Both tables are long: thousands of ATECO codes on one side, and on the other, INAIL’s groups, broken down into hundreds of tariff items, each with its own description. And there’s no official correspondence between the two. Matching them means reading descriptions and interpreting them, and that has always required a professional. It still does, but this is where the tool described on this page comes in: to make that work faster and more efficient.
My client, an occupational physician, asked me to develop it for his website: a company enters its ATECO code (or its INAIL group) and the number of workers, and gets its likely first-aid group in seconds. The tool does the first pass; the professional takes it from there.
The tool
On the website, the visitor picks either an INAIL group or an ATECO code, then the number of workers. One click, and the result appears with a traffic light:
| Group | Mandatory course | Refresher | First-aid equipment |
|---|---|---|---|
| 🔴 A | 16 hours | every 3 years, 6 hours | first-aid box |
| 🟡 B | 12 hours | every 3 years, 4 hours | first-aid box |
| 🟢 C | 12 hours | every 3 years, 4 hours | basic medical kit |
When the ATECO code could belong to groups with different outcomes, the tool doesn’t guess. It says the case is ambiguous, and invites the company to ask an expert.
From ATECO to INAIL: semantic search
The INAIL route is simple: each INAIL group has an official risk index, and above 4 the company is in Group A. The hard part is the ATECO route: deciding which INAIL group each of the 3,257 ATECO codes belongs to. After discussing it with my client, I computed that mapping in advance, once2, and stored it in a table in the WordPress database, which the tool looks up.
To build it, I used semantic search. Each description is turned into an embedding, a list of numbers that captures its meaning, so texts with similar meanings end up close together even when they use different words. Then, for each ATECO code, the system looks for the closest INAIL groups:
- Three views of each INAIL group: the official wording, a plain-language description and a list of keywords. Formal, descriptive and keyword-like ATECO titles can each find a close match.
- Context from the hierarchy: each ATECO title is searched together with its parent categories, so a vague title like “other activities…” is still matched in the right sector.
- An adaptive cut-off: instead of keeping a fixed number of results (say, the top three), the system keeps candidates up to the first unusually large drop in similarity. A clear-cut match gets one candidate; a fuzzy one gets several.
- A weighted vote: when candidates disagree, each votes according to how close it is. A group needs more than 60% to win; otherwise the code is marked uncertain.
- Ambiguity only matters when it changes the answer: if every candidate is above 4, or every one is at or below 4, the verdict is certain even when the exact INAIL group isn’t.
Every code also gets a one-line explanation of how its verdict was reached: that’s for the human in the loop.
A human in the loop
Pure semantic similarity makes odd suggestions. Among its candidates, the search proposed agricultural machinery for aircraft manufacturing, and tourism and catering for data processing. So every run was followed by a review.
I went through the codes the pipeline had flagged as uncertain one by one3, around ninety of them, and decided where each belonged. For a few tricky ones, I asked ChatGPT for a second opinion. Then, instead of patching the final table, I fed each decision back into the search index as a short keyword “anchor”: for example, “battery manufacturing” pointing to the chemicals group.
That choice mattered. A patched table forgets its fixes the next time it’s rebuilt. An anchor in the index stays there: every new run reproduces the fix, and it also pulls similar codes in the right direction. Over several runs, the index grew from 117 descriptions to 165, and each new version was compared with the previous one before going online.
Results and limits
In the final run, all 3,257 ATECO codes got a verdict:
- 1,101 in Group A
- 2,108 not in Group A (so B or C, depending on the number of workers)
- 48 uncertain, sent to an expert rather than guessed
Almost every code was settled by full agreement among its candidates. Only a handful needed the weighted vote.
The limits are worth stating plainly:
- Similarity isn’t understanding. Semantic search finds descriptions that sound alike, which isn’t always the same as activities that carry the same risk. That’s why the review step exists, and why it will always be needed.
- The data ages. The tool uses the INAIL indices published in June 2022 and the ATECO 2025 classification. When either changes, the mapping has to be rebuilt and checked again.
- It’s a first pass, not a verdict. The result helps a company understand where it probably stands. The final word belongs to the professional.
- Ministerial Decree 388/2003 (Decreto Ministeriale 15 luglio 2003, n. 388). I’m neither a lawyer nor a doctor, so please check the details with a professional. ↩︎
- Well, it took a few attempts and plenty of tests to get it as right as possible. That’s the part where a human has to check the AI’s work. ↩︎
- I set up the pipeline to flag them automatically: whenever the candidate groups disagreed and none got more than 60% of the vote, the code was marked uncertain. And each new run was compared with the previous one, so any code whose verdict changed stood out for review. ↩︎
