• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

inclusion-numerique / coop-mediation-numerique / 14ba6690-9c33-4ca2-ab73-fa1fd8aa6215

21 May 2026 08:58AM UTC coverage: 9.992% (+3.0%) from 7.009%
14ba6690-9c33-4ca2-ab73-fa1fd8aa6215

Pull #497

circleci

marc-gavanier
feat: improve structure fusion scoring and review export

Significantly reduces the manual review burden by detecting more
true duplicates automatically and avoiding false positives.

Scoring improvements (detect-duplicate-structures, generate-structures-action-plan):
- Treat clusters of type 'mixte' like 'doublon_certain' with per-pair
  scoring (instead of bulk verification_manuelle), uncovering hundreds
  of auto/probable fusions previously hidden in mixed clusters.
- Boost address score to 1.0 when one normalized address is contained
  in the other (e.g. "Lupino" vs "LUPINO PARVIS NOTRE DAME VICTOIRE").
- Add address abbreviations: VC (voie communale), RT (route), ZA, ZI, CH.
- Redistribute geo weight when coords are unavailable, OR when address
  strongly indicates the same place (>=0.85): prevents penalizing
  structures with missing or erroneous coords.
- Normalize "commune de/du", "mairie de/du", "ville de/du" to a single
  "ville" canonical token so variants match.
- Detect "service keywords" (EPN, médiathèque, CCAS, France services,
  MJC, etc.): when one name has such a keyword and the other does not,
  they are distinct entities even with shared SIRET/address. Disables
  the address-contained heuristic and keeps geo in the score.

Sync resilience (findOrCreateStructure):
- After strict siret+codeInsee miss, fall back to siret-only with
  normalized contained-name match. This catches Dataspace structures
  whose codeInsee diverges from the coop's, without merging an EPN
  with its parent town hall (asymmetric-service-keyword guard).

Review output:
- generate-structures-action-plan: structures-fusion-review.csv now
  uses cluster-grouped format (CIBLE + sources + empty line between
  clusters, sorted by ascending score), matching the existing format
  Tim uses for his manual reviews.
- export-duplicate-sirets: cluster-grouped CSV (empty line between
  SIRETs) and exclude empty-string siret. Enrich each row with
  nom_api, adresse_api, corre... (continued)
Pull Request #497: feat: improve structure fusion scoring and review export

688 of 10876 branches covered (6.33%)

Branch coverage included in aggregate %.

26 of 150 new or added lines in 4 files covered. (17.33%)

911 existing lines in 95 files now uncovered.

2111 of 17137 relevant lines covered (12.32%)

1.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/apps/web/src/worksheet/buildWorksheetHelpers.ts
1
import type { SessionUser } from '@app/web/auth/sessionUser'
2
import type {
3
  ActivitesFiltersLabels,
4
  FilterType,
5
} from '@app/web/features/activites/use-cases/list/components/generateActivitesFiltersLabels'
6
import { addTitleRow } from '@app/web/libs/worksheet/addTitleRow'
7
import type { Worksheet } from 'exceljs'
8

UNCOV
9
const onlyType = (type: string) => (filter: { type: string }) =>
×
UNCOV
10
  filter.type === type
×
11

UNCOV
12
const toLabel = ({ label }: { label: string }) => label
×
13

14
export const addFilters =
UNCOV
15
  (worksheet: Worksheet) =>
×
UNCOV
16
  (
×
17
    filters: ActivitesFiltersLabels,
18
    {
19
      mediateurScope,
20
      excludeFilters = [],
×
21
    }: {
22
      mediateurScope: null | Pick<SessionUser, 'firstName' | 'lastName'>
23
      excludeFilters?: FilterType[]
24
    },
25
  ) => {
UNCOV
26
    addTitleRow(worksheet)('Filtres')
×
27

UNCOV
28
    return worksheet.addRows(
×
29
      [
30
        !excludeFilters.includes('periode')
×
31
          ? [
32
              'Période',
UNCOV
33
              filters.find((filter) => filter.type === 'periode')?.label ?? '-',
×
34
            ]
35
          : undefined,
36
        !excludeFilters.includes('source')
×
37
          ? [
38
              'Source',
UNCOV
39
              filters.find((filter) => filter.type === 'source')?.label ?? '-',
×
40
            ]
41
          : undefined,
42
        !excludeFilters.includes('lieux')
×
43
          ? [
44
              'Lieux d’accompagnement',
45
              filters
×
46
                .filter(onlyType('lieux'))
UNCOV
47
                .map(({ label }) => label)
×
48
                .join(', ') || '-',
49
            ]
50
          : undefined,
51
        !excludeFilters.includes('communes')
×
52
          ? [
53
              'Communes',
54
              filters.filter(onlyType('communes')).map(toLabel).join(', ') ||
×
55
                '-',
56
            ]
57
          : undefined,
58
        !excludeFilters.includes('departements')
×
59
          ? [
60
              'Départements',
61
              filters
×
62
                .filter(onlyType('departements'))
63
                .map(toLabel)
64
                .join(', ') || '-',
65
            ]
66
          : undefined,
67
        !excludeFilters.includes('types')
×
68
          ? [
69
              'Type d’accompagnement',
70
              filters.filter(onlyType('types')).map(toLabel).join(', ') || '-',
×
71
            ]
72
          : undefined,
73
        !excludeFilters.includes('conseiller_numerique')
×
74
          ? [
75
              'Rôle',
UNCOV
76
              filters.find((filter) => filter.type === 'conseiller_numerique')
×
77
                ?.label ?? '-',
78
            ]
79
          : undefined,
80
        !excludeFilters.includes('thematiqueNonAdministratives')
×
81
          ? [
82
              'Thématiques non administratives',
83
              filters
×
84
                .filter(onlyType('thematiqueNonAdministratives'))
85
                .map(toLabel)
86
                .join(', ') || '-',
87
            ]
88
          : undefined,
89
        !excludeFilters.includes('thematiqueAdministratives')
×
90
          ? [
91
              'Thématiques administratives',
92
              filters
×
93
                .filter(onlyType('thematiqueAdministratives'))
94
                .map(toLabel)
95
                .join(', ') || '-',
96
            ]
97
          : undefined,
98
        !excludeFilters.includes('tags')
×
99
          ? [
100
              'Tags',
101
              filters.filter(onlyType('tags')).map(toLabel).join(', ') || '-',
×
102
            ]
103
          : undefined,
104
        !excludeFilters.includes('beneficiaires') &&
×
105
        filters.filter(onlyType('beneficiaires')).length > 0
106
          ? [
107
              'Bénéficiaires',
108
              filters
×
109
                .filter(onlyType('beneficiaires'))
110
                .map(toLabel)
111
                .join(', ') || '-',
112
            ]
113
          : undefined,
114
        !excludeFilters.includes('mediateurs') &&
×
115
        filters.filter(onlyType('mediateurs')).length > 0
116
          ? [
117
              'Médiateurs',
118
              filters.filter(onlyType('mediateurs')).map(toLabel).join(', ') ||
×
119
                '-',
120
            ]
121
          : undefined,
122
        mediateurScope
×
123
          ? [
124
              'Médiateur',
125
              `${mediateurScope.firstName} ${mediateurScope.lastName}`,
126
            ]
127
          : undefined,
128
        [],
129
      ].filter(Boolean),
130
    )
131
  }
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc