gedcom-ts 2026.5.1 → 2026.5.3

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,33 @@
2
2
 
3
3
  All notable changes of gedcom-ts
4
4
 
5
- ## [Unreleased]
5
+ ## [2026.5.3] - 2026-05-16
6
+
7
+ ### Added
8
+
9
+ - **Module `gedcom-ts/booklet`** : génération de livrets généalogiques en PDF (collecte des personnes, récits en français, frises chronologiques, estimation de taille, `generateGenealogyBookletPdf`, `downloadBookletPdf`). Dépendance `pdf-lib` incluse dans ce sous-paquet.
10
+ - **Logo couverture** : chemins SVG `GEDCOM_TS_LOGO_PATHS` / `GEDCOM_TS_LOGO_VIEWBOX` et `drawGedcomTsLogoOnPage` exportés ; logo gedcom-ts sur la couverture par défaut (`coverLogo` dans `BookletPdfOptions`).
11
+ - **Tests** : `tests/booklet/` (date/lieu/genre, logo, PDF smoke).
12
+ - **README** : guide `gedcom-ts/booklet`, double point d’entrée npm.
13
+
14
+ ### Changed
15
+
16
+ - **`findHarmonizationClustersFromActs`** : si plusieurs libellés similaires partagent une seule position GPS et que tous les actes ont des coordonnées, unifie automatiquement le libellé (libellé le plus long) sans cluster d’harmonisation manuel.
17
+ - Critère d’harmonisation : ne signale plus les seules variantes d’orthographe lorsque les coordonnées sont déjà identiques.
18
+
19
+ ## [2026.5.2] - 2026-05-16
20
+
21
+ ### Added
22
+
23
+ - **Regroupement des lieux** : `groupActsBySimilarCity`, `actsNeedingGeocodeForCity` (alias géoloc), `similarCityKey` ; harmonisation et carte alignées sur `citiesAreSimilar` (plus de divergence `normalizeCityKey` seul).
24
+
25
+ ### Changed
26
+
27
+ - **`findHarmonizationClustersFromActs`** : union-find sur `citiesAreSimilar` (corrige le cas Pleurtuit 29+11 actes après géoloc complète).
28
+ - **`clusterKeyForCity`** : clé carte basée sur la localité principale (segment avant la virgule), alignée avec le regroupement par similarité.
29
+ - **README** : guide d’intégration géoloc restructuré (section dédiée, tableaux par cas d’usage) ; retrait de la documentation développement interne.
30
+
31
+ ## [2026.5.1] - 2026-05-15
6
32
 
7
33
  ### Added
8
34
 
@@ -101,7 +127,7 @@ Les entrées historiques du changelog conservent leurs numéros semver d’origi
101
127
 
102
128
  - Major GEDCOM 7 coverage push: round-trip preservation (`preservedTopLevelRecords`, label-keyed FAM / NOTE xrefs, level-0 `OBJE`), richer `HEAD` export options, full P1.4 dates and places (INT/EST/CAL, `3 PHRASE`, `3 TIME`, `2 SDATE`, `2 PHRASE` event-level, ISO date input, verbatim fallback).
103
129
  - New **fluent edit layer** (`editPerson`, `editAct`, `editActs`, `editDateAct`, `editPlace`, `editNotes` + `*Edit` classes) for in-place model mutations with chainable APIs.
104
- - **README fully rewritten** with a complete public-API reference and a link to the live graphical demo at **[https://gedcomts.jaunet.me](https://gedcomts.jaunet.me)**.
130
+ - **README fully rewritten** with a complete public-API reference and a link to the live graphical demo at **[https://gedcomts.com](https://gedcomts.com)**.
105
131
 
106
132
  ### Added
107
133
 
@@ -162,7 +188,7 @@ Les entrées historiques du changelog conservent leurs numéros semver d’origi
162
188
  #### Documentation
163
189
 
164
190
  - **README intégralement réécrit** : référence complète de l’API publique (un sous-titre par export de `src/index.ts`), tables synthétiques pour `ReadGed`, `GedcomExportOptions` et la couche `*Edit`, exemples mis à jour pour chaque type / classe / fonction exposés.
165
- - Mention en tête du README du **site de démo graphique** : **[https://gedcomts.jaunet.me](https://gedcomts.jaunet.me)**.
191
+ - Mention en tête du README du **site de démo graphique** : **[https://gedcomts.com](https://gedcomts.com)**.
166
192
 
167
193
  ### Changed
168
194
 
package/README.md CHANGED
@@ -6,16 +6,29 @@
6
6
  - work with a typed JSON model (persons, acts, dates, places, notes, media, name variants, attributes)
7
7
  - edit the model in-place through a fluent, chainable API (`editPerson`, `editAct`, …)
8
8
  - export data back to GEDCOM (`.ged`) or GEDZIP (`.zip`)
9
+ - geocode event places (OpenStreetMap / Nominatim) and group similar city names
10
+ - generate a **genealogy booklet** as PDF (`gedcom-ts/booklet`, French narratives)
9
11
 
10
12
  ## Live demo
11
13
 
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)**
14
+ A graphical demo showcasing the public API (import a `.ged` / `.zip`, browse the typed model, export back) is available at **[https://gedcomts.com](https://gedcomts.com)**
13
15
 
14
- ## Project
16
+ ## Contents
15
17
 
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)
18
+ - [Installation](#installation)
19
+ - [Quick start](#quick-start)
20
+ - [Geocoding places](#geocoding-places)
21
+ - [Genealogy booklet (PDF)](#genealogy-booklet-pdf)
22
+ - [API reference](#api-reference)
23
+ - [Error handling](#error-handling)
24
+
25
+ ## Package
26
+
27
+ - NPM: [gedcom-ts](https://www.npmjs.com/package/gedcom-ts)
28
+ - Version format **CalVer** `AAAA.M.micro` (e.g. `2026.5.3` = May 2026). See [CHANGELOG.md](CHANGELOG.md).
29
+ - Entry points:
30
+ - **`gedcom-ts`** — import, model, edit layer, export, geocoding
31
+ - **`gedcom-ts/booklet`** — PDF livret (`pdf-lib` bundled in that chunk)
19
32
 
20
33
  ## Installation
21
34
 
@@ -23,6 +36,8 @@ A graphical demo showcasing the public API (import a `.ged` / `.zip`, browse the
23
36
  npm install gedcom-ts
24
37
  ```
25
38
 
39
+ Both entry points come from the same package; no extra install for the booklet.
40
+
26
41
  ## Runtime Requirements
27
42
 
28
43
  - modern browser runtime (`File`, `Blob`, `XMLHttpRequest`, `URL.createObjectURL`)
@@ -51,9 +66,243 @@ Typical workflow:
51
66
  2. read / mutate the typed `Person`, `Act`, `Place`, `Note`, `MultimediaFile` objects (directly or through `editPerson` / `editAct` / `editPlace` / …)
52
67
  3. export as `.ged` (`ExportGedcomFile`) or `.zip` (`ExportGedzipFile`)
53
68
 
54
- ## Public API reference
69
+ ## Geocoding places
70
+
71
+ 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.
72
+
73
+ You work with a `ReadGed` (after import) and a flat `Act[]` built from every person’s events (see [Prepare act list](#prepare-act-list)).
74
+
75
+ ### How city names are matched
76
+
77
+ The library groups variants of the same place with **`citiesAreSimilar`**, not exact string equality.
78
+
79
+
80
+ | Label A | Label B | Same place? |
81
+ | ----------- | ------------------------------------ | --------------------------- |
82
+ | `Pleurtuit` | `Pleurtuit, Ille-et-Vilaine, France` | Yes |
83
+ | `Paris` | `Paris, TX, USA` | Depends on similarity rules |
84
+
85
+
86
+ 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).
87
+
88
+ ### Prepare act list
89
+
90
+ ```ts
91
+ import type { ReadGed, Act } from "gedcom-ts";
92
+
93
+ function collectAllActs(ged: ReadGed): Act[] {
94
+ const acts: Act[] = [];
95
+ for (const person of ged.persons) {
96
+ for (const act of person.acts.list) acts.push(act);
97
+ }
98
+ return acts;
99
+ }
100
+ ```
101
+
102
+ ### Geocode one city (typical flow)
103
+
104
+ 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.
105
+
106
+
107
+ | Step | Function |
108
+ | ---------------------------------------------- | ----------------------------------------------------------- |
109
+ | 1. Context from the tree (countries, centroid) | `inferGeocodeContext(ged)` |
110
+ | 2. Search OpenStreetMap | `searchPlacesWithContext(cityName, context)` |
111
+ | 3. Acts to update | `actsNeedingGeocodeForCity(allActs, cityName)` |
112
+ | 4. Write coordinates | `applyGeocodeCandidateToActs(targets, candidate, cityName)` |
113
+
114
+
115
+ ```ts
116
+ import {
117
+ inferGeocodeContext,
118
+ searchPlacesWithContext,
119
+ actsNeedingGeocodeForCity,
120
+ applyGeocodeCandidateToActs,
121
+ } from "gedcom-ts";
122
+ import type { ReadGed, Act } from "gedcom-ts";
123
+
124
+ async function geocodeCity(ged: ReadGed, allActs: Act[], cityName: string) {
125
+ const context = inferGeocodeContext(ged);
126
+ const candidates = await searchPlacesWithContext(cityName, context);
127
+ const chosen = candidates[0];
128
+ if (!chosen) return;
129
+
130
+ const targets = actsNeedingGeocodeForCity(allActs, cityName);
131
+ applyGeocodeCandidateToActs(targets, chosen, cityName);
132
+ }
133
+ ```
134
+
135
+ > **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`.
136
+
137
+ ### List cities missing coordinates
138
+
139
+ One row per city; `withoutCoordCount` is how many acts still need GPS.
140
+
141
+ ```ts
142
+ import { groupActsBySimilarCity } from "gedcom-ts";
143
+
144
+ const groups = groupActsBySimilarCity(allActs, { onlyWithoutCoordinates: true });
145
+
146
+ for (const group of groups) {
147
+ console.log(group.cityLabel, group.withoutCoordCount);
148
+ // group.acts — acts in this group without GPS
149
+ }
150
+ ```
151
+
152
+ Largest groups first (`group.acts.length`).
153
+
154
+ ### Map: one marker per city
155
+
156
+ ```ts
157
+ import { clusterKeyForCity } from "gedcom-ts";
158
+
159
+ const markerKey = clusterKeyForCity(act.place?.city ?? "");
160
+ // merge markers that share the same markerKey
161
+ ```
162
+
163
+ ### Harmonization (data quality, optional)
164
+
165
+ Use when spellings, GPS positions, or “some acts with / without GPS” disagree for the same similar city.
166
+
167
+ ```ts
168
+ import { findHarmonizationClusters } from "gedcom-ts";
169
+
170
+ for (const cluster of findHarmonizationClusters(ged)) {
171
+ console.log(cluster.labels, cluster.coordVariants, cluster.actsWithoutCoord);
172
+ }
173
+ ```
174
+
175
+ After a full geocode via `actsNeedingGeocodeForCity`, you should not get a cluster that only reports missing GPS for that city.
176
+
177
+ ### Geocoding API cheat sheet
178
+
179
+
180
+ | Goal | Call |
181
+ | ------------------------- | ---------------------------------------------------------------- |
182
+ | Search | `searchPlacesWithContext(city, inferGeocodeContext(ged))` |
183
+ | Acts to update on confirm | `actsNeedingGeocodeForCity(acts, city)` |
184
+ | Apply lat/lng | `applyGeocodeCandidateToActs(targets, candidate, city)` |
185
+ | Cities without GPS | `groupActsBySimilarCity(acts, { onlyWithoutCoordinates: true })` |
186
+ | Map marker id | `clusterKeyForCity(city)` |
187
+ | Inconsistencies | `findHarmonizationClusters(ged)` |
188
+
189
+
190
+ 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.
191
+
192
+ The legacy callback `getCityCoordinates` is deprecated — use the flow above.
193
+
194
+ ## Genealogy booklet (PDF)
195
+
196
+ The **`gedcom-ts/booklet`** subpath builds a printable family booklet: cover page, table of contents, chapters by generation (Sosa), family sheets, and French narrative text from GEDCOM acts (birth, marriage, death, …). Optional generation timelines are rasterized for PDF.
197
+
198
+ ### Workflow
199
+
200
+ 1. Import with `importGedFile` (`gedcom-ts`).
201
+ 2. Collect persons with `collectBookletPersons` (`gedcom-ts/booklet`).
202
+ 3. Optionally preview size with `estimateBookletSize` + `groupBookletIntoChapters`.
203
+ 4. Build bytes with `generateGenealogyBookletPdf`, then `downloadBookletPdf` (browser).
204
+
205
+ ```ts
206
+ import { importGedFile } from "gedcom-ts";
207
+ import {
208
+ collectBookletPersons,
209
+ groupBookletIntoChapters,
210
+ estimateBookletSize,
211
+ generateGenealogyBookletPdf,
212
+ downloadBookletPdf,
213
+ personDisplayName,
214
+ } from "gedcom-ts/booklet";
215
+
216
+ async function exportBooklet(file: File) {
217
+ const ged = await importGedFile(file);
218
+ const root = ged.persons[0] ?? null;
219
+
220
+ const entries = collectBookletPersons({
221
+ ged,
222
+ scope: "from-reference",
223
+ referencePerson: root,
224
+ maxGeneration: 6,
225
+ });
226
+
227
+ const chapters = groupBookletIntoChapters(entries);
228
+ const families = chapters.reduce((n, ch) => n + ch.families.length, 0);
229
+ const size = estimateBookletSize(chapters, entries.length, families, "summary", "canvas");
230
+ console.log(size.label);
231
+
232
+ const pdf = await generateGenealogyBookletPdf(
233
+ entries,
234
+ {
235
+ title: "Livret familial",
236
+ scopeLabel: "Ancêtres et descendance",
237
+ personCount: entries.length,
238
+ referenceName: root ? personDisplayName(root) : undefined,
239
+ },
240
+ {
241
+ detailLevel: "summary",
242
+ timelineStyle: "canvas",
243
+ coverLogo: true,
244
+ },
245
+ );
246
+
247
+ downloadBookletPdf(pdf, "livret.pdf");
248
+ }
249
+ ```
250
+
251
+ Use `scope: "all"` and `referencePerson: null` to include every individual in the file.
252
+
253
+ ### `collectBookletPersons` options
254
+
255
+ | Option | Role |
256
+ | --- | --- |
257
+ | `ged` | `ReadGed` after import |
258
+ | `scope` | `"all"` or `"from-reference"` (Sosa from `referencePerson`) |
259
+ | `referencePerson` | Root person for `"from-reference"`; `null` if scope is `"all"` |
260
+ | `maxGeneration` | Max Sosa generation (e.g. `6`); ignored when `scope === "all"` |
261
+
262
+ Helpers: `personDisplayName`, `buildBookletPersonEntry`, `sortBookletEntries`, `bookletSexFromPerson`.
263
+
264
+ ### `generateGenealogyBookletPdf` options
265
+
266
+ | Option | Values | Effect |
267
+ | --- | --- | --- |
268
+ | `detailLevel` | `"summary"` \| `"detailed"` | Short prose per person vs longer biographies |
269
+ | `timelineStyle` | `"off"` \| `"canvas"` | Generation timeline pages per chapter |
270
+ | `coverLogo` | `true` (default), `false`, or `DrawGedcomTsLogoOptions` | gedcom-ts vector logo on the cover |
271
+
272
+ `BookletPdfMeta`: `title`, `scopeLabel`, `personCount`, optional `referenceName`.
273
+
274
+ ### Cover logo
275
+
276
+ The cover draws the **gedcom-ts logo** from paths shipped in the package (`GEDCOM_TS_LOGO_PATHS`, `GEDCOM_TS_LOGO_VIEWBOX`). Reuse on custom PDF pages:
277
+
278
+ ```ts
279
+ import { PDFDocument } from "pdf-lib";
280
+ import {
281
+ drawGedcomTsLogoOnPage,
282
+ defaultCoverLogoOptions,
283
+ BOOKLET_BRAND_RGB,
284
+ } from "gedcom-ts/booklet";
285
+
286
+ const doc = await PDFDocument.create();
287
+ const page = doc.addPage();
288
+ drawGedcomTsLogoOnPage(page, defaultCoverLogoOptions());
289
+ ```
290
+
291
+ ### Booklet API cheat sheet
292
+
293
+ | Goal | Export |
294
+ | --- | --- |
295
+ | Persons for the booklet | `collectBookletPersons` |
296
+ | Chapters / families | `groupBookletIntoChapters`, `partnerNamesLabel` |
297
+ | French narratives (custom UI) | `buildPersonSummaryNarrative`, `buildPersonDetailedNarratives`, `buildFamilyNarrative`, `buildChapterIntroNarrative` |
298
+ | Page estimate | `estimateBookletSize`, `bookletSizeAdvice` |
299
+ | Timeline data / PNG | `buildGenerationTimeline`, `rasterizeGenerationTimelinePng` |
300
+ | PDF output | `generateGenealogyBookletPdf`, `downloadBookletPdf`, `toPdfText` |
301
+ | Logo | `drawGedcomTsLogoOnPage`, `defaultCoverLogoOptions`, `GEDCOM_TS_LOGO_PATHS` |
302
+
303
+ ## API reference
55
304
 
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`.
305
+ Short description and a minimal snippet for each export of **`gedcom-ts`**. For the PDF booklet, see [Genealogy booklet (PDF)](#genealogy-booklet-pdf) (`gedcom-ts/booklet`).
57
306
 
58
307
  ### Importing a file
59
308
 
@@ -104,22 +353,24 @@ Result of an import. Top-level GEDCOM records not modeled into the typed graph (
104
353
 
105
354
  Notable members:
106
355
 
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. |
356
+
357
+ | Member | Description |
358
+ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
359
+ | `persons: Person[]` | Imported individuals. |
360
+ | `mapPersons: Map<number, Person>` | `INDI` (integer)`Person`. |
361
+ | `partnersMap: Map<number, Person[]>` | Family id → spouses. |
362
+ | `childsMap: Map<number, Person[]>` | Family idchildren. |
363
+ | `placesMap: Map<string, Place>` | City name → `Place` (first occurrence wins). |
364
+ | `mapFiles: Map<string, File>` | Relative path → media `File` (for ZIP imports). |
365
+ | `datasetVersion: GedcomDatasetVersion` | `"7.0"`, `"5.5"` or `"unknown"`. |
366
+ | `preservedTopLevelRecords: string[]` | Raw blocks for partial round-trip. |
367
+ | `resolveIndividualPointer(raw)` | Resolves `@I12@`, `I12`, `@Homer_Simpson@`, `Homer_Simpson` to a `Person`. |
368
+ | `getChildrenForParent(parent)` | All children attached to a parent (via `FAMS`). |
369
+ | `getChildrenOfFamily(familyId)` | Children of a single family. |
370
+ | `groupPartners()` | Rebuilds `partnersMap` / `childsMap` after editing links. |
371
+ | `generateUniqueIndi()` | Next free `INDI` number for new persons. |
372
+ | `rehydratePlacesFromActs()` | Rebuilds `placesMap` from act places (e.g. after manual graph edits). Use `editReadGed(readGed).addPerson(...)` to register persons so maps stay coherent. |
373
+
123
374
 
124
375
  ```ts
125
376
  import { ReadGed } from "gedcom-ts";
@@ -177,13 +428,15 @@ async function exportZip(persons: Person[]) {
177
428
 
178
429
  Options shared by both exporters:
179
430
 
180
- | Option | Effect |
181
- | --- | --- |
431
+
432
+ | Option | Effect |
433
+ | ---------------------- | -------------------------------------------------------------------------------------------------------- |
182
434
  | `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 …`. |
435
+ | `headLanguageTag` | BCP 47 tag for `HEAD`.`LANG` (defaults to `en-US`). |
436
+ | `headCopyright` | One-line `1 COPR` notice. |
437
+ | `headDestination` | Value of `HEAD`.`DEST` (target app / URI). |
438
+ | `headSchemaTagDefs` | Extension tag definitions emitted as `HEAD`.`SCHMA` / `2 TAG …`. |
439
+
187
440
 
188
441
  ### Domain model
189
442
 
@@ -394,14 +647,16 @@ editPerson(person)
394
647
  editDateAct(person.acts.list[0].dateAct!).setExactDate(1900, "JAN", 1);
395
648
  ```
396
649
 
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`. |
650
+
651
+ | Helper / class | Purpose |
652
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
653
+ | `editPerson(person)` / `PersonEdit` | Identity, `FAMS` / `FAMC` / `removeFams*`, `nameVariants()` / `attributes()` / `multimedia()`, bulk `clear*`, entry points to `acts()` and `notes()`. |
654
+ | `editActs(acts)` / `ActsEdit` | `add`, `addNew`, `insertAt`, `insertNewAt`, `replaceAt`, `removeAt`, `removeAct`, `removeLast`, `clear`, `sortByDate`, `at`, `indexOfAct`. |
655
+ | `editAct(act)` / `ActEdit` | `setType`, `setIndis`, dates, place, EVEN fields, preserved lines, `notes()`, `multimedia()`, `clearNotes`, `clearMultimedia`. |
656
+ | `editDateAct(dateAct)` / `DateActEdit` | `clear`, `applyGedcomPayload`, `setExactDate`, `setQualified`, `setBetween`, `setFromTo`, `setTime`, `setDatePhrase` / `appendDatePhrase`, `setVerbatimPayload`. |
657
+ | `editPlace(place)` / `PlaceEdit` | `setFromGedcom7Payload`, `setCity`, `setCounty`, `setState`, `setCountry`, `setPlacPhrase` / `appendPlacPhrase` / `clearPlacPhrase`, `setCoordinates` / `clearCoordinates` / `replaceCoordinateModel`, `clearStructured`. |
658
+ | `editNotes(notes)` / `NotesEdit` | `add`, `addNew`, `insertAt`, `insertNewAt`, `replaceAt`, `removeAt`, `removeNote`, `removeLast`, `clear`, `at`, `indexOfNote`. |
659
+
405
660
 
406
661
  ### Dataset editing: `ReadGed`, graph, clone, export options, validation
407
662
 
@@ -459,15 +714,17 @@ validateReadGed(readGed, {
459
714
 
460
715
  `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
716
 
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. |
717
+
718
+ | Export | Role |
719
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
720
+ | `editReadGed(readGed)` / `ReadGedEdit` | `addPerson`, `removePersonByIndi`, `preserved()` `PreservedTopLevelEdit` (`append`, `insertAt`, `replaceAt`, `removeAt`, `clear`) on `preservedTopLevelRecords`. Low-level helpers: `addPersonToReadGed`, `removePersonFromReadGedByIndi`. |
721
+ | `editGedcomExportOptions(opts)` / `GedcomExportOptionsEdit` | Fluent setters for `extraTopLevelRecords`, `headLanguageTag`, `headCopyright`, `headDestination`, `headSchemaTagDefs`. |
722
+ | `clonePerson(person, newIndi)` / `cloneAct(act)` / `person.clone` / `act.clone` | Deep copies for templates or undo stacks. |
723
+ | `nextFamilyId(persons)` | Next internal family id `F`. |
724
+ | `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`. |
725
+ | `validateReadGed`, `assertReadGedConsistent`, `validatePerson`, `assertPersonConsistent` | Typed `code` / `severity` (`error` \| `warn`). Options: `checkMarrParticipants`, `checkDuplicateIndis`, `checkFamcWithoutSpouses`, `checkFamsWithoutSpouses`, `checkDuplicateFamsEntries`, `checkAncestorCycles`. Assertions: `failOn` (default: `error`). |
726
+ | `tryAddPersonToReadGed`, `tryRemovePersonFromReadGedByIndi`, `tryLinkChildToFamily`, `tryUnlinkChildFromFamily`, `tryCreateMarriageFamily`, `commandBlockingIssues` | Command layer with `CommandResult` / `validate*Command` prechecks. |
727
+
471
728
 
472
729
  ### Utilities
473
730
 
@@ -492,31 +749,9 @@ import { remainingTypesAct, Acts } from "gedcom-ts";
492
749
  const available = remainingTypesAct(new Acts());
493
750
  ```
494
751
 
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
752
  #### `getCityCoordinates(cityName, callback)` (deprecated)
518
753
 
519
- Legacy callback API; delegates to `searchPlaces` and maps results to `Place[]`. Prefer `searchPlaces` / `searchPlacesWithContext`.
754
+ Legacy callback API. Use [Geocoding places](#geocoding-places) instead.
520
755
 
521
756
  #### `resolveDatasetVersion(headerLines)` / `GedcomDatasetVersion`
522
757
 
@@ -561,7 +796,7 @@ const primary = selectPrimaryNameVariant(person.nameVariants);
561
796
 
562
797
  #### `GEDCOM_LIBRARY_VERSION`
563
798
 
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.
799
+ CalVer string written in exported `HEAD`.`SOUR`.`VERS` (same value as the npm package version, e.g. `2026.5.2`).
565
800
 
566
801
  ```ts
567
802
  import { GEDCOM_LIBRARY_VERSION } from "gedcom-ts";
@@ -569,56 +804,6 @@ import { GEDCOM_LIBRARY_VERSION } from "gedcom-ts";
569
804
  console.log(`gedcom-ts ${GEDCOM_LIBRARY_VERSION}`);
570
805
  ```
571
806
 
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
807
  ## Error handling
623
808
 
624
809
  ```ts
@@ -635,3 +820,4 @@ try {
635
820
  }
636
821
  }
637
822
  ```
823
+
@@ -0,0 +1,8 @@
1
+ /** Date réduite à une année (éventuellement avec qualificateur GEDCOM). */
2
+ export declare function isYearOnlyDate(dateLabel: string): boolean;
3
+ /** « le 15 mars 1991 », « en 1991 », « vers 1700 », « avant 1800 »… */
4
+ export declare function onDate(dateLabel: string): string;
5
+ /** « De 1700 à 1850 » ou « Du 15 mars 1700 au 3 janvier 1850 ». */
6
+ export declare function lifeDateRange(from: string, to: string): string;
7
+ /** « voit le jour en 1991 » ou « voit le jour le 15 mars 1991 ». */
8
+ export declare function seesDayOn(dateLabel: string): string;
@@ -0,0 +1,11 @@
1
+ import type { BookletTimelineStyle } from './booklet-timeline';
2
+ import type { BookletChapter, BookletDetailLevel } from './booklet-structure';
3
+ export interface BookletSizeEstimate {
4
+ readonly pages: number;
5
+ readonly label: string;
6
+ }
7
+ /** Nombre de pages frise estimé pour un chapitre (tranches dynamiques). */
8
+ export declare function estimateTimelineSliceCount(rowCount: number): number;
9
+ /** Estimation grossière du nombre de pages pour orienter l’utilisateur. */
10
+ export declare function estimateBookletSize(chapters: readonly BookletChapter[], personCount: number, familyCount: number, detailLevel: BookletDetailLevel, timelineStyle?: BookletTimelineStyle): BookletSizeEstimate;
11
+ export declare function bookletSizeAdvice(personCount: number, detailLevel: BookletDetailLevel): string | null;
@@ -0,0 +1,23 @@
1
+ /** Sexe GEDCOM exploité pour les accords du livret (M, F, U, X). */
2
+ export type BookletSex = 'M' | 'F' | 'U' | 'X';
3
+ export interface PersonGenderWords {
4
+ readonly born: string;
5
+ readonly died: string;
6
+ readonly baptized: string;
7
+ readonly present: string;
8
+ readonly linked: string;
9
+ readonly offspring: string;
10
+ readonly descendant: string;
11
+ readonly childOf: string;
12
+ readonly parentOf: string;
13
+ }
14
+ /** Participes / noms accordés au genre de la personne. U et X → formes neutres. */
15
+ export declare function personGenderWords(sex: BookletSex): PersonGenderWords;
16
+ /** Suffixe pluriel français (0 ou 1 → singulier). */
17
+ export declare function pluralS(count: number): string;
18
+ /** Choisit la forme singulier / pluriel. */
19
+ export declare function pluralPick<T>(count: number, one: T, many: T): T;
20
+ /** « 1 enfant recensé » / « N enfants recensés ». */
21
+ export declare function recensedChildrenPhrase(count: number): string;
22
+ /** « 1 autre union citée… » / « N autres unions citées… ». */
23
+ export declare function otherUnionsPhrase(count: number): string;
@@ -0,0 +1,19 @@
1
+ import { type PDFPage, type RGB } from 'pdf-lib';
2
+ export { GEDCOM_TS_LOGO_PATHS, GEDCOM_TS_LOGO_VIEWBOX } from "./gedcom-ts-logo.paths";
3
+ /** Couleur de marque gedcom-ts (#19196f), pour pdf-lib `rgb(...)`. */
4
+ export declare const BOOKLET_BRAND_RGB: RGB;
5
+ export interface GedcomTsLogoLayout {
6
+ /** Ordonnée PDF du bas du logo (pour placer texte et barre en dessous). */
7
+ readonly bottomY: number;
8
+ readonly height: number;
9
+ }
10
+ export interface DrawGedcomTsLogoOptions {
11
+ readonly pageWidth?: number;
12
+ readonly maxWidth?: number;
13
+ readonly bottomY?: number;
14
+ readonly color?: RGB;
15
+ }
16
+ /** Options par défaut du logo sur une page A4 (couverture du livret). */
17
+ export declare function defaultCoverLogoOptions(pageWidth?: number, pageHeight?: number): DrawGedcomTsLogoOptions;
18
+ /** Dessine le logo gedcom-ts en vecteur SVG sur la page (pdf-lib drawSvgPath). */
19
+ export declare function drawGedcomTsLogoOnPage(page: PDFPage, options?: DrawGedcomTsLogoOptions): GedcomTsLogoLayout;