gedcom-ts 2026.5.1 → 2026.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,19 @@
2
2
 
3
3
  All notable changes of gedcom-ts
4
4
 
5
- ## [Unreleased]
5
+ ## [2026.5.2] - 2026-05-16
6
+
7
+ ### Added
8
+
9
+ - **Regroupement des lieux** : `groupActsBySimilarCity`, `actsNeedingGeocodeForCity` (alias géoloc), `similarCityKey` ; harmonisation et carte alignées sur `citiesAreSimilar` (plus de divergence `normalizeCityKey` seul).
10
+
11
+ ### Changed
12
+
13
+ - **`findHarmonizationClustersFromActs`** : union-find sur `citiesAreSimilar` (corrige le cas Pleurtuit 29+11 actes après géoloc complète).
14
+ - **`clusterKeyForCity`** : clé carte basée sur la localité principale (segment avant la virgule), alignée avec le regroupement par similarité.
15
+ - **README** : guide d’intégration géoloc restructuré (section dédiée, tableaux par cas d’usage) ; retrait de la documentation développement interne.
16
+
17
+ ## [2026.5.1] - 2026-05-15
6
18
 
7
19
  ### Added
8
20
 
package/README.md CHANGED
@@ -11,11 +11,18 @@
11
11
 
12
12
  A graphical demo showcasing the public API (import a `.ged` / `.zip`, browse the typed model, export back) is available at **[https://gedcomts.jaunet.me](https://gedcomts.jaunet.me)**
13
13
 
14
- ## Project
14
+ ## Contents
15
15
 
16
- - NPM package: [gedcom-ts](https://www.npmjs.com/package/gedcom-ts)
17
- - **Versioning (CalVer)** : releases use **`AAAA.M.micro`** (e.g. `2026.5.0` = May 2026). The npm `version`, `GEDCOM_LIBRARY_VERSION` (export `HEAD`.`SOUR`.`VERS`), and release branches share the same label. Older changelog entries still refer to semver (`2.1.0`, …). See [CHANGELOG.md](CHANGELOG.md).
18
- - **GEDCOM 7 roadmap** (spec gedcom.io, coverage matrix, prioritized gaps): [docs/GEDCOM7-roadmap.md](docs/GEDCOM7-roadmap.md)
16
+ - [Installation](#installation)
17
+ - [Quick start](#quick-start)
18
+ - [Geocoding places](#geocoding-places)
19
+ - [API reference](#api-reference)
20
+ - [Error handling](#error-handling)
21
+
22
+ ## Package
23
+
24
+ - NPM: [gedcom-ts](https://www.npmjs.com/package/gedcom-ts)
25
+ - Version format **CalVer** `AAAA.M.micro` (e.g. `2026.5.2` = May 2026). See [CHANGELOG.md](CHANGELOG.md).
19
26
 
20
27
  ## Installation
21
28
 
@@ -51,9 +58,134 @@ Typical workflow:
51
58
  2. read / mutate the typed `Person`, `Act`, `Place`, `Note`, `MultimediaFile` objects (directly or through `editPerson` / `editAct` / `editPlace` / …)
52
59
  3. export as `.ged` (`ExportGedcomFile`) or `.zip` (`ExportGedzipFile`)
53
60
 
54
- ## Public API reference
61
+ ## Geocoding places
62
+
63
+ Use this section when your app needs **GPS coordinates** on event places, a **list of cities still without coordinates**, **map markers**, or a **data-quality** view for inconsistent place names.
64
+
65
+ You work with a `ReadGed` (after import) and a flat `Act[]` built from every person’s events (see [Prepare act list](#prepare-act-list)).
66
+
67
+ ### How city names are matched
68
+
69
+ The library groups variants of the same place with **`citiesAreSimilar`**, not exact string equality.
70
+
71
+
72
+ | Label A | Label B | Same place? |
73
+ | ----------- | ------------------------------------ | --------------------------- |
74
+ | `Pleurtuit` | `Pleurtuit, Ille-et-Vilaine, France` | Yes |
75
+ | `Paris` | `Paris, TX, USA` | Depends on similarity rules |
76
+
77
+
78
+ Use the functions in the tables below for grouping, geocoding, and maps. Do **not** compare raw strings yourself, and do **not** use `normalizeCityKey` for grouping (spelling normalization only).
79
+
80
+ ### Prepare act list
81
+
82
+ ```ts
83
+ import type { ReadGed, Act } from "gedcom-ts";
84
+
85
+ function collectAllActs(ged: ReadGed): Act[] {
86
+ const acts: Act[] = [];
87
+ for (const person of ged.persons) {
88
+ for (const act of person.acts.list) acts.push(act);
89
+ }
90
+ return acts;
91
+ }
92
+ ```
93
+
94
+ ### Geocode one city (typical flow)
95
+
96
+ When the user picks a city and a Nominatim result, update **every similar act that still has no GPS** — not only acts with the exact same label.
97
+
98
+
99
+ | Step | Function |
100
+ | ---------------------------------------------- | ----------------------------------------------------------- |
101
+ | 1. Context from the tree (countries, centroid) | `inferGeocodeContext(ged)` |
102
+ | 2. Search OpenStreetMap | `searchPlacesWithContext(cityName, context)` |
103
+ | 3. Acts to update | `actsNeedingGeocodeForCity(allActs, cityName)` |
104
+ | 4. Write coordinates | `applyGeocodeCandidateToActs(targets, candidate, cityName)` |
105
+
106
+
107
+ ```ts
108
+ import {
109
+ inferGeocodeContext,
110
+ searchPlacesWithContext,
111
+ actsNeedingGeocodeForCity,
112
+ applyGeocodeCandidateToActs,
113
+ } from "gedcom-ts";
114
+ import type { ReadGed, Act } from "gedcom-ts";
115
+
116
+ async function geocodeCity(ged: ReadGed, allActs: Act[], cityName: string) {
117
+ const context = inferGeocodeContext(ged);
118
+ const candidates = await searchPlacesWithContext(cityName, context);
119
+ const chosen = candidates[0];
120
+ if (!chosen) return;
121
+
122
+ const targets = actsNeedingGeocodeForCity(allActs, cityName);
123
+ applyGeocodeCandidateToActs(targets, chosen, cityName);
124
+ }
125
+ ```
126
+
127
+ > **Tip:** Do not pass only acts from a harmonization cluster to `applyGeocodeCandidateToActs`. Longer labels (e.g. `Pleurtuit, Ille-et-Vilaine, France`) would stay without GPS. Always use `actsNeedingGeocodeForCity`.
128
+
129
+ ### List cities missing coordinates
130
+
131
+ One row per city; `withoutCoordCount` is how many acts still need GPS.
132
+
133
+ ```ts
134
+ import { groupActsBySimilarCity } from "gedcom-ts";
135
+
136
+ const groups = groupActsBySimilarCity(allActs, { onlyWithoutCoordinates: true });
137
+
138
+ for (const group of groups) {
139
+ console.log(group.cityLabel, group.withoutCoordCount);
140
+ // group.acts — acts in this group without GPS
141
+ }
142
+ ```
143
+
144
+ Largest groups first (`group.acts.length`).
145
+
146
+ ### Map: one marker per city
147
+
148
+ ```ts
149
+ import { clusterKeyForCity } from "gedcom-ts";
150
+
151
+ const markerKey = clusterKeyForCity(act.place?.city ?? "");
152
+ // merge markers that share the same markerKey
153
+ ```
154
+
155
+ ### Harmonization (data quality, optional)
156
+
157
+ Use when spellings, GPS positions, or “some acts with / without GPS” disagree for the same similar city.
158
+
159
+ ```ts
160
+ import { findHarmonizationClusters } from "gedcom-ts";
161
+
162
+ for (const cluster of findHarmonizationClusters(ged)) {
163
+ console.log(cluster.labels, cluster.coordVariants, cluster.actsWithoutCoord);
164
+ }
165
+ ```
55
166
 
56
- Everything exported from `gedcom-ts` is documented below with a short description and a minimal usage snippet. The full list mirrors the public exports of `src/index.ts`.
167
+ After a full geocode via `actsNeedingGeocodeForCity`, you should not get a cluster that only reports missing GPS for that city.
168
+
169
+ ### Geocoding API cheat sheet
170
+
171
+
172
+ | Goal | Call |
173
+ | ------------------------- | ---------------------------------------------------------------- |
174
+ | Search | `searchPlacesWithContext(city, inferGeocodeContext(ged))` |
175
+ | Acts to update on confirm | `actsNeedingGeocodeForCity(acts, city)` |
176
+ | Apply lat/lng | `applyGeocodeCandidateToActs(targets, candidate, city)` |
177
+ | Cities without GPS | `groupActsBySimilarCity(acts, { onlyWithoutCoordinates: true })` |
178
+ | Map marker id | `clusterKeyForCity(city)` |
179
+ | Inconsistencies | `findHarmonizationClusters(ged)` |
180
+
181
+
182
+ Network: `GET https://nominatim.openstreetmap.org/search?q=…&format=jsonv2` with a `User-Agent` (`gedcom-ts/<version> (genealogy library)`). For offline or mocked search, pass `fetchFn` in `searchPlaces` / `searchPlacesWithContext` options.
183
+
184
+ The legacy callback `getCityCoordinates` is deprecated — use the flow above.
185
+
186
+ ## API reference
187
+
188
+ Short description and a minimal snippet for each public export.
57
189
 
58
190
  ### Importing a file
59
191
 
@@ -104,22 +236,24 @@ Result of an import. Top-level GEDCOM records not modeled into the typed graph (
104
236
 
105
237
  Notable members:
106
238
 
107
- | Member | Description |
108
- | --- | --- |
109
- | `persons: Person[]` | Imported individuals. |
110
- | `mapPersons: Map<number, Person>` | `INDI` (integer) → `Person`. |
111
- | `partnersMap: Map<number, Person[]>` | Family idspouses. |
112
- | `childsMap: Map<number, Person[]>` | Family id → children. |
113
- | `placesMap: Map<string, Place>` | City name`Place` (first occurrence wins). |
114
- | `mapFiles: Map<string, File>` | Relative pathmedia `File` (for ZIP imports). |
115
- | `datasetVersion: GedcomDatasetVersion` | `"7.0"`, `"5.5"` or `"unknown"`. |
116
- | `preservedTopLevelRecords: string[]` | Raw blocks for partial round-trip. |
117
- | `resolveIndividualPointer(raw)` | Resolves `@I12@`, `I12`, `@Homer_Simpson@`, `Homer_Simpson` to a `Person`. |
118
- | `getChildrenForParent(parent)` | All children attached to a parent (via `FAMS`). |
119
- | `getChildrenOfFamily(familyId)` | Children of a single family. |
120
- | `groupPartners()` | Rebuilds `partnersMap` / `childsMap` after editing links. |
121
- | `generateUniqueIndi()` | Next free `INDI` number for new persons. |
122
- | `rehydratePlacesFromActs()` | Rebuilds `placesMap` from act places (e.g. after manual graph edits). Use `editReadGed(readGed).addPerson(...)` to register persons so maps stay coherent. |
239
+
240
+ | Member | Description |
241
+ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
242
+ | `persons: Person[]` | Imported individuals. |
243
+ | `mapPersons: Map<number, Person>` | `INDI` (integer)`Person`. |
244
+ | `partnersMap: Map<number, Person[]>` | Family id → spouses. |
245
+ | `childsMap: Map<number, Person[]>` | Family idchildren. |
246
+ | `placesMap: Map<string, Place>` | City name → `Place` (first occurrence wins). |
247
+ | `mapFiles: Map<string, File>` | Relative path → media `File` (for ZIP imports). |
248
+ | `datasetVersion: GedcomDatasetVersion` | `"7.0"`, `"5.5"` or `"unknown"`. |
249
+ | `preservedTopLevelRecords: string[]` | Raw blocks for partial round-trip. |
250
+ | `resolveIndividualPointer(raw)` | Resolves `@I12@`, `I12`, `@Homer_Simpson@`, `Homer_Simpson` to a `Person`. |
251
+ | `getChildrenForParent(parent)` | All children attached to a parent (via `FAMS`). |
252
+ | `getChildrenOfFamily(familyId)` | Children of a single family. |
253
+ | `groupPartners()` | Rebuilds `partnersMap` / `childsMap` after editing links. |
254
+ | `generateUniqueIndi()` | Next free `INDI` number for new persons. |
255
+ | `rehydratePlacesFromActs()` | Rebuilds `placesMap` from act places (e.g. after manual graph edits). Use `editReadGed(readGed).addPerson(...)` to register persons so maps stay coherent. |
256
+
123
257
 
124
258
  ```ts
125
259
  import { ReadGed } from "gedcom-ts";
@@ -177,13 +311,15 @@ async function exportZip(persons: Person[]) {
177
311
 
178
312
  Options shared by both exporters:
179
313
 
180
- | Option | Effect |
181
- | --- | --- |
314
+
315
+ | Option | Effect |
316
+ | ---------------------- | -------------------------------------------------------------------------------------------------------- |
182
317
  | `extraTopLevelRecords` | Raw `0 …` blocks re-emitted before `SUBM` / `TRLR` (round-trip with `readGed.preservedTopLevelRecords`). |
183
- | `headLanguageTag` | BCP 47 tag for `HEAD`.`LANG` (defaults to `en-US`). |
184
- | `headCopyright` | One-line `1 COPR` notice. |
185
- | `headDestination` | Value of `HEAD`.`DEST` (target app / URI). |
186
- | `headSchemaTagDefs` | Extension tag definitions emitted as `HEAD`.`SCHMA` / `2 TAG …`. |
318
+ | `headLanguageTag` | BCP 47 tag for `HEAD`.`LANG` (defaults to `en-US`). |
319
+ | `headCopyright` | One-line `1 COPR` notice. |
320
+ | `headDestination` | Value of `HEAD`.`DEST` (target app / URI). |
321
+ | `headSchemaTagDefs` | Extension tag definitions emitted as `HEAD`.`SCHMA` / `2 TAG …`. |
322
+
187
323
 
188
324
  ### Domain model
189
325
 
@@ -394,14 +530,16 @@ editPerson(person)
394
530
  editDateAct(person.acts.list[0].dateAct!).setExactDate(1900, "JAN", 1);
395
531
  ```
396
532
 
397
- | Helper / class | Purpose |
398
- | --- | --- |
399
- | `editPerson(person)` / `PersonEdit` | Identity, `FAMS` / `FAMC` / `removeFams*`, `nameVariants()` / `attributes()` / `multimedia()`, bulk `clear*`, entry points to `acts()` and `notes()`. |
400
- | `editActs(acts)` / `ActsEdit` | `add`, `addNew`, `insertAt`, `insertNewAt`, `replaceAt`, `removeAt`, `removeAct`, `removeLast`, `clear`, `sortByDate`, `at`, `indexOfAct`. |
401
- | `editAct(act)` / `ActEdit` | `setType`, `setIndis`, dates, place, EVEN fields, preserved lines, `notes()`, `multimedia()`, `clearNotes`, `clearMultimedia`. |
402
- | `editDateAct(dateAct)` / `DateActEdit` | `clear`, `applyGedcomPayload`, `setExactDate`, `setQualified`, `setBetween`, `setFromTo`, `setTime`, `setDatePhrase` / `appendDatePhrase`, `setVerbatimPayload`. |
403
- | `editPlace(place)` / `PlaceEdit` | `setFromGedcom7Payload`, `setCity`, `setCounty`, `setState`, `setCountry`, `setPlacPhrase` / `appendPlacPhrase` / `clearPlacPhrase`, `setCoordinates` / `clearCoordinates` / `replaceCoordinateModel`, `clearStructured`. |
404
- | `editNotes(notes)` / `NotesEdit` | `add`, `addNew`, `insertAt`, `insertNewAt`, `replaceAt`, `removeAt`, `removeNote`, `removeLast`, `clear`, `at`, `indexOfNote`. |
533
+
534
+ | Helper / class | Purpose |
535
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
536
+ | `editPerson(person)` / `PersonEdit` | Identity, `FAMS` / `FAMC` / `removeFams*`, `nameVariants()` / `attributes()` / `multimedia()`, bulk `clear*`, entry points to `acts()` and `notes()`. |
537
+ | `editActs(acts)` / `ActsEdit` | `add`, `addNew`, `insertAt`, `insertNewAt`, `replaceAt`, `removeAt`, `removeAct`, `removeLast`, `clear`, `sortByDate`, `at`, `indexOfAct`. |
538
+ | `editAct(act)` / `ActEdit` | `setType`, `setIndis`, dates, place, EVEN fields, preserved lines, `notes()`, `multimedia()`, `clearNotes`, `clearMultimedia`. |
539
+ | `editDateAct(dateAct)` / `DateActEdit` | `clear`, `applyGedcomPayload`, `setExactDate`, `setQualified`, `setBetween`, `setFromTo`, `setTime`, `setDatePhrase` / `appendDatePhrase`, `setVerbatimPayload`. |
540
+ | `editPlace(place)` / `PlaceEdit` | `setFromGedcom7Payload`, `setCity`, `setCounty`, `setState`, `setCountry`, `setPlacPhrase` / `appendPlacPhrase` / `clearPlacPhrase`, `setCoordinates` / `clearCoordinates` / `replaceCoordinateModel`, `clearStructured`. |
541
+ | `editNotes(notes)` / `NotesEdit` | `add`, `addNew`, `insertAt`, `insertNewAt`, `replaceAt`, `removeAt`, `removeNote`, `removeLast`, `clear`, `at`, `indexOfNote`. |
542
+
405
543
 
406
544
  ### Dataset editing: `ReadGed`, graph, clone, export options, validation
407
545
 
@@ -459,15 +597,17 @@ validateReadGed(readGed, {
459
597
 
460
598
  `tryAddPersonToReadGed`, `tryRemovePersonFromReadGedByIndi`, `tryLinkChildToFamily`, `tryUnlinkChildFromFamily`, and `tryCreateMarriageFamily` return a `CommandResult` (`ok` + `issues` or `value` + optional `warnings`). `editReadGed(...).addPerson` uses these checks internally (throws on blocking errors). For UI or transactional flows, prefer the `try*` APIs and inspect `commandBlockingIssues`.
461
599
 
462
- | Export | Role |
463
- | --- | --- |
464
- | `editReadGed(readGed)` / `ReadGedEdit` | `addPerson`, `removePersonByIndi`, `preserved()` → `PreservedTopLevelEdit` (`append`, `insertAt`, `replaceAt`, `removeAt`, `clear`) on `preservedTopLevelRecords`. Low-level helpers: `addPersonToReadGed`, `removePersonFromReadGedByIndi`. |
465
- | `editGedcomExportOptions(opts)` / `GedcomExportOptionsEdit` | Fluent setters for `extraTopLevelRecords`, `headLanguageTag`, `headCopyright`, `headDestination`, `headSchemaTagDefs`. |
466
- | `clonePerson(person, newIndi)` / `cloneAct(act)` / `person.clone` / `act.clone` | Deep copies for templates or undo stacks. |
467
- | `nextFamilyId(persons)` | Next internal family id `F`. |
468
- | `createMarriageFamily`, `linkChildToFamily`, `unlinkChildFromFamily`, `removeFamilyReferencesFromDataset` | Crée une `F` et des actes sur les deux conjoints. 5ᵉ argument : `{ eventTag }` (défaut `MARR`) pour `ENGA`, bans, contrat, `EVEN`+`CreateActInit`, etc. Voir `GEDCOM_7_PAIR_UNION_EVENT_TAGS`. |
469
- | `validateReadGed`, `assertReadGedConsistent`, `validatePerson`, `assertPersonConsistent` | Typed `code` / `severity` (`error` \| `warn`). Options: `checkMarrParticipants`, `checkDuplicateIndis`, `checkFamcWithoutSpouses`, `checkFamsWithoutSpouses`, `checkDuplicateFamsEntries`, `checkAncestorCycles`. Assertions: `failOn` (default: `error`). |
470
- | `tryAddPersonToReadGed`, `tryRemovePersonFromReadGedByIndi`, `tryLinkChildToFamily`, `tryUnlinkChildFromFamily`, `tryCreateMarriageFamily`, `commandBlockingIssues` | Command layer with `CommandResult` / `validate*Command` prechecks. |
600
+
601
+ | Export | Role |
602
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
603
+ | `editReadGed(readGed)` / `ReadGedEdit` | `addPerson`, `removePersonByIndi`, `preserved()` `PreservedTopLevelEdit` (`append`, `insertAt`, `replaceAt`, `removeAt`, `clear`) on `preservedTopLevelRecords`. Low-level helpers: `addPersonToReadGed`, `removePersonFromReadGedByIndi`. |
604
+ | `editGedcomExportOptions(opts)` / `GedcomExportOptionsEdit` | Fluent setters for `extraTopLevelRecords`, `headLanguageTag`, `headCopyright`, `headDestination`, `headSchemaTagDefs`. |
605
+ | `clonePerson(person, newIndi)` / `cloneAct(act)` / `person.clone` / `act.clone` | Deep copies for templates or undo stacks. |
606
+ | `nextFamilyId(persons)` | Next internal family id `F`. |
607
+ | `createMarriageFamily`, `linkChildToFamily`, `unlinkChildFromFamily`, `removeFamilyReferencesFromDataset` | Create family `F` and spouse acts; optional `{ eventTag }` (default `MARR`, also `ENGA`, banns, contract, `EVEN` + `CreateActInit`, …). See `GEDCOM_7_PAIR_UNION_EVENT_TAGS`. |
608
+ | `validateReadGed`, `assertReadGedConsistent`, `validatePerson`, `assertPersonConsistent` | Typed `code` / `severity` (`error` \| `warn`). Options: `checkMarrParticipants`, `checkDuplicateIndis`, `checkFamcWithoutSpouses`, `checkFamsWithoutSpouses`, `checkDuplicateFamsEntries`, `checkAncestorCycles`. Assertions: `failOn` (default: `error`). |
609
+ | `tryAddPersonToReadGed`, `tryRemovePersonFromReadGedByIndi`, `tryLinkChildToFamily`, `tryUnlinkChildFromFamily`, `tryCreateMarriageFamily`, `commandBlockingIssues` | Command layer with `CommandResult` / `validate*Command` prechecks. |
610
+
471
611
 
472
612
  ### Utilities
473
613
 
@@ -492,31 +632,9 @@ import { remainingTypesAct, Acts } from "gedcom-ts";
492
632
  const available = remainingTypesAct(new Acts());
493
633
  ```
494
634
 
495
- #### Geocoding (Nominatim)
496
-
497
- Place search uses the official Nominatim endpoint `GET https://nominatim.openstreetmap.org/search` with `q`, `format=jsonv2`, and `addressdetails=1` (not the legacy `search.php?city=` API). A `User-Agent` header is required (`gedcom-ts/<version> (genealogy library)` by default).
498
-
499
- ```ts
500
- import {
501
- inferGeocodeContext,
502
- searchPlacesWithContext,
503
- rankGeocodeCandidates,
504
- findHarmonizationClusters,
505
- applyGeocodeCandidateToActs,
506
- } from "gedcom-ts";
507
-
508
- const context = inferGeocodeContext(ged);
509
- const candidates = await searchPlacesWithContext("Valence", context);
510
- // candidates are ranked (French tree → Valence FR before ES)
511
-
512
- const clusters = findHarmonizationClusters(ged);
513
- ```
514
-
515
- For tests or Node without a global `fetch`, pass `fetchFn` in options. Low-level API: `searchPlaces(query, { countryCodes, limit, userAgent, fetchFn })`.
516
-
517
635
  #### `getCityCoordinates(cityName, callback)` (deprecated)
518
636
 
519
- Legacy callback API; delegates to `searchPlaces` and maps results to `Place[]`. Prefer `searchPlaces` / `searchPlacesWithContext`.
637
+ Legacy callback API. Use [Geocoding places](#geocoding-places) instead.
520
638
 
521
639
  #### `resolveDatasetVersion(headerLines)` / `GedcomDatasetVersion`
522
640
 
@@ -561,7 +679,7 @@ const primary = selectPrimaryNameVariant(person.nameVariants);
561
679
 
562
680
  #### `GEDCOM_LIBRARY_VERSION`
563
681
 
564
- CalVer string embedded in the exported `HEAD`.`SOUR`.`VERS` (same value as `package.json` `version`, e.g. `2026.5.0`). A test (`version-package-sync`) enforces the match on every CI run.
682
+ CalVer string written in exported `HEAD`.`SOUR`.`VERS` (same value as the npm package version, e.g. `2026.5.2`).
565
683
 
566
684
  ```ts
567
685
  import { GEDCOM_LIBRARY_VERSION } from "gedcom-ts";
@@ -569,56 +687,6 @@ import { GEDCOM_LIBRARY_VERSION } from "gedcom-ts";
569
687
  console.log(`gedcom-ts ${GEDCOM_LIBRARY_VERSION}`);
570
688
  ```
571
689
 
572
- ## End-to-end example
573
-
574
- ```ts
575
- import {
576
- importGedFile,
577
- ExportGedzipFile,
578
- editPerson,
579
- DateAct,
580
- Identifier,
581
- } from "gedcom-ts";
582
-
583
- async function importModifyExport(file: File) {
584
- const readGed = await importGedFile(file);
585
- const persons = readGed.persons;
586
-
587
- if (persons.length > 0) {
588
- editPerson(persons[0])
589
- .setLastname(persons[0].lastname.toUpperCase())
590
- .acts()
591
- .at(0)
592
- .setDateAct(new DateAct("1 JAN 1900"));
593
- }
594
-
595
- await new ExportGedzipFile("updated-tree", persons).download();
596
- }
597
- ```
598
-
599
- ## Development
600
-
601
- ### Local scripts
602
-
603
- | Script | Role |
604
- | --- | --- |
605
- | `npm run lint` | ESLint on the codebase |
606
- | `npm run test` | Vitest test suite |
607
- | `npm run build` | Production bundle + `.d.ts` |
608
- | `npm run tgz` | `build` then `npm pack` (local `.tgz`) |
609
-
610
- ### GitLab CI (`.gitlab-ci.yml`)
611
-
612
- Every pipeline runs **`InstallDependencies`** (`npm ci`, `node_modules` artifact) — required by all other jobs.
613
-
614
- | Job | When it runs |
615
- | --- | --- |
616
- | `InstallDependencies` | Always |
617
- | `Lint`, `Test` | Merge requests and branch pushes (not on `master` after a merge commit titled `Merge…`) |
618
- | `BuildTgz`, `PublishNpm` | Push to `master` after merge only (`Merge…` commit title) |
619
-
620
- Typical flow: open an MR from a release branch (e.g. `2026.5.0`) → lint + test run there → after merge to `master`, only package build and npm publish run (quality checks are not repeated).
621
-
622
690
  ## Error handling
623
691
 
624
692
  ```ts
@@ -635,3 +703,4 @@ try {
635
703
  }
636
704
  }
637
705
  ```
706
+
@@ -1,3 +1,3 @@
1
1
  export { normalizeCityKey, tidyCityDisplay, findCanonicalCityLabel, } from "./place-city-utils";
2
2
  export { type GeocodeCandidate, type GeocodeContext, type GeocodeFetchFn, type GeocodeSearchOptions, NOMINATIM_SEARCH_URL, defaultGeocodeUserAgent, parseCityQuery, countryLabelToIso, isoToCountryLabel, inferGeocodeContext, buildGeocodeQuery, geocodeContextWithHint, rankGeocodeCandidates, searchPlaces, searchPlacesWithContext, } from "./place-geocode";
3
- export { type CoordVariant, type CityHarmonizationCluster, levenshteinDistance, citiesAreSimilar, clusterKeyForCity, findHarmonizationClustersFromActs, findHarmonizationClusters, applyCoordinatesToActs, applyGeocodeCandidateToActs, } from "./place-clusters";
3
+ export { type CoordVariant, type CityHarmonizationCluster, type SimilarCityActGroup, type GroupActsBySimilarCityOptions, type FilterActsBySimilarCityOptions, levenshteinDistance, citiesAreSimilar, clusterKeyForCity, similarCityKey, groupActsBySimilarCity, filterActsBySimilarCity, actsNeedingGeocodeForCity, findHarmonizationClustersFromActs, findHarmonizationClusters, applyCoordinatesToActs, applyGeocodeCandidateToActs, } from "./place-clusters";
@@ -1,11 +1,15 @@
1
1
  import type { Act } from "../commons/Act";
2
2
  import type { ReadGed } from "../import/ReadGed";
3
- /** Clé de regroupement : casse, espaces, compatibilité Unicode (NFKC). */
3
+ /**
4
+ * Égalité stricte de libellé (casse / espaces / NFKC uniquement).
5
+ * Pour regrouper des variantes (« Pleurtuit » vs « Pleurtuit, Ille-et-Vilaine »),
6
+ * utiliser {@link citiesAreSimilar}, {@link clusterKeyForCity} ou {@link groupActsBySimilarCity}.
7
+ */
4
8
  export declare function normalizeCityKey(city: string): string;
5
9
  /** Libellé affiché : trim + espaces internes unifiés (sans forcer la casse). */
6
10
  export declare function tidyCityDisplay(city: string): string;
7
11
  /**
8
- * Harmonise l’écriture avec le reste de l’arbre : même clé normalisée → libellé le plus fréquent
9
- * (exclut l’acte en cours d’édition pour le décompte).
12
+ * Libellé canonique par **égalité stricte** {@link normalizeCityKey} (pas `citiesAreSimilar`).
13
+ * Pour une ville « proche », préférer le `cityLabel` d’un {@link groupActsBySimilarCity}.
10
14
  */
11
15
  export declare function findCanonicalCityLabel(ged: ReadGed, cityInput: string, excludeAct: Act | null): string;
@@ -14,13 +14,54 @@ export interface CityHarmonizationCluster {
14
14
  readonly acts: readonly Act[];
15
15
  readonly actsWithoutCoord: number;
16
16
  }
17
+ /** Groupe d’actes partageant une même ville au sens {@link citiesAreSimilar}. */
18
+ export interface SimilarCityActGroup {
19
+ readonly cityLabel: string;
20
+ /** Même clé que {@link clusterKeyForCity} / carte. */
21
+ readonly clusterKey: string;
22
+ readonly acts: readonly Act[];
23
+ readonly withoutCoordCount: number;
24
+ }
25
+ export interface GroupActsBySimilarCityOptions {
26
+ /** Ne retient que les actes sans coordonnées GPS utilisables. */
27
+ readonly onlyWithoutCoordinates?: boolean;
28
+ }
29
+ export interface FilterActsBySimilarCityOptions {
30
+ readonly onlyWithoutCoordinates?: boolean;
31
+ }
17
32
  /** Distance de Levenshtein (petites chaînes de noms de villes). */
18
33
  export declare function levenshteinDistance(a: string, b: string): number;
19
- /** Ville proche : même clé, faute légère, ou inclusion évidente (Saint-X / St-X). */
34
+ /**
35
+ * Ville proche : même clé décorée, faute légère, ou inclusion évidente (Saint-X / St-X).
36
+ * **Règle unique** pour regrouper actes, carte et harmonisation.
37
+ */
20
38
  export declare function citiesAreSimilar(a: string, b: string): boolean;
21
39
  /**
22
- * Regroupe les actes dont les villes se ressemblent (libellés uniques + clés normalisées).
23
- * Complexité ~ O(m²) sur le nombre de villes distinctes, pas sur le nombre d’actes.
40
+ * Clé stable pour la carte et les listes « par ville ».
41
+ * Utilise la localité principale (segment avant la première virgule), comme le regroupement
42
+ * par {@link citiesAreSimilar}, afin que « Pleurtuit » et « Pleurtuit, Ille-et-Vilaine, France »
43
+ * partagent la même clé.
44
+ */
45
+ export declare function clusterKeyForCity(city: string): string;
46
+ /** Alias explicite de {@link clusterKeyForCity}. */
47
+ export declare const similarCityKey: typeof clusterKeyForCity;
48
+ /**
49
+ * Regroupe les actes par ville similaire (tri décroissant par nombre d’actes).
50
+ * Utiliser pour une UI « lieux sans position » (`onlyWithoutCoordinates: true`).
51
+ */
52
+ export declare function groupActsBySimilarCity(acts: readonly Act[], options?: GroupActsBySimilarCityOptions): SimilarCityActGroup[];
53
+ /**
54
+ * Actes dont le lieu est similaire à `cityRef` (même logique que l’harmonisation / la carte).
55
+ */
56
+ export declare function filterActsBySimilarCity(acts: readonly Act[], cityRef: string, options?: FilterActsBySimilarCityOptions): Act[];
57
+ /**
58
+ * Actes similaires à `cityRef` sans coordonnées GPS — à géolocaliser en une fois.
59
+ * Alias de `filterActsBySimilarCity(..., { onlyWithoutCoordinates: true })`.
60
+ */
61
+ export declare function actsNeedingGeocodeForCity(acts: readonly Act[], cityRef: string): Act[];
62
+ /**
63
+ * Conflits d’harmonisation : libellés multiples, positions GPS divergentes,
64
+ * ou mixte avec/sans coordonnées sur la même ville similaire.
24
65
  */
25
66
  export declare function findHarmonizationClustersFromActs(acts: readonly Act[]): CityHarmonizationCluster[];
26
67
  /** Analyse toute l’arbre (préférer {@link findHarmonizationClustersFromActs} sur un sous-ensemble). */
@@ -30,5 +71,3 @@ export declare function applyCoordinatesToActs(acts: readonly Act[], lat: number
30
71
  country?: string | null;
31
72
  }): void;
32
73
  export declare function applyGeocodeCandidateToActs(acts: readonly Act[], candidate: GeocodeCandidate, preferredCityLabel?: string): void;
33
- /** Clé de regroupement carte : villes similaires → un marqueur. */
34
- export declare function clusterKeyForCity(city: string): string;