gedcom-ts 2026.5.0 → 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 +21 -1
- package/README.md +189 -106
- package/dist/geocode/index.d.ts +3 -0
- package/dist/geocode/place-city-utils.d.ts +15 -0
- package/dist/geocode/place-clusters.d.ts +73 -0
- package/dist/geocode/place-geocode.d.ts +50 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +1 -1
- package/dist/services/CitySearch.d.ts +4 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes of gedcom-ts
|
|
4
4
|
|
|
5
|
-
## [
|
|
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
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **`src/geocode/`** : géocodage Nominatim (`GET /search?q=…&format=jsonv2`), contexte arbre (`inferGeocodeContext`), ranking (`rankGeocodeCandidates`), harmonisation des lieux (`findHarmonizationClusters`, `applyGeocodeCandidateToActs`), utilitaires ville (`normalizeCityKey`, `findCanonicalCityLabel`). `fetch` et `User-Agent` configurables pour les tests.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **`getCityCoordinates`** : marqué `@deprecated` ; utilise la nouvelle API Nominatim (plus `search.php?city=` ni `XMLHttpRequest`).
|
|
6
26
|
|
|
7
27
|
## [2026.5.0] - 2026-05-15
|
|
8
28
|
|
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
|
-
##
|
|
14
|
+
## Contents
|
|
15
15
|
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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
|
-
##
|
|
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
|
+
```
|
|
166
|
+
|
|
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)` |
|
|
55
180
|
|
|
56
|
-
|
|
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
|
-
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
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 id → children. |
|
|
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
|
-
|
|
181
|
-
|
|
|
314
|
+
|
|
315
|
+
| Option | Effect |
|
|
316
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
182
317
|
| `extraTopLevelRecords` | Raw `0 …` blocks re-emitted before `SUBM` / `TRLR` (round-trip with `readGed.preservedTopLevelRecords`). |
|
|
183
|
-
| `headLanguageTag`
|
|
184
|
-
| `headCopyright`
|
|
185
|
-
| `headDestination`
|
|
186
|
-
| `headSchemaTagDefs`
|
|
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
|
-
|
|
398
|
-
|
|
|
399
|
-
|
|
|
400
|
-
| `
|
|
401
|
-
| `
|
|
402
|
-
| `
|
|
403
|
-
| `
|
|
404
|
-
| `
|
|
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
|
-
|
|
463
|
-
|
|
|
464
|
-
|
|
|
465
|
-
| `
|
|
466
|
-
| `
|
|
467
|
-
| `
|
|
468
|
-
| `
|
|
469
|
-
| `
|
|
470
|
-
| `
|
|
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,17 +632,9 @@ import { remainingTypesAct, Acts } from "gedcom-ts";
|
|
|
492
632
|
const available = remainingTypesAct(new Acts());
|
|
493
633
|
```
|
|
494
634
|
|
|
495
|
-
#### `getCityCoordinates(cityName, callback)`
|
|
635
|
+
#### `getCityCoordinates(cityName, callback)` (deprecated)
|
|
496
636
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
```ts
|
|
500
|
-
import { getCityCoordinates } from "gedcom-ts";
|
|
501
|
-
|
|
502
|
-
getCityCoordinates("Paris", (places) => {
|
|
503
|
-
console.log(places[0]?.coordinate.latitude, places[0]?.coordinate.longitude);
|
|
504
|
-
});
|
|
505
|
-
```
|
|
637
|
+
Legacy callback API. Use [Geocoding places](#geocoding-places) instead.
|
|
506
638
|
|
|
507
639
|
#### `resolveDatasetVersion(headerLines)` / `GedcomDatasetVersion`
|
|
508
640
|
|
|
@@ -547,7 +679,7 @@ const primary = selectPrimaryNameVariant(person.nameVariants);
|
|
|
547
679
|
|
|
548
680
|
#### `GEDCOM_LIBRARY_VERSION`
|
|
549
681
|
|
|
550
|
-
CalVer string
|
|
682
|
+
CalVer string written in exported `HEAD`.`SOUR`.`VERS` (same value as the npm package version, e.g. `2026.5.2`).
|
|
551
683
|
|
|
552
684
|
```ts
|
|
553
685
|
import { GEDCOM_LIBRARY_VERSION } from "gedcom-ts";
|
|
@@ -555,56 +687,6 @@ import { GEDCOM_LIBRARY_VERSION } from "gedcom-ts";
|
|
|
555
687
|
console.log(`gedcom-ts ${GEDCOM_LIBRARY_VERSION}`);
|
|
556
688
|
```
|
|
557
689
|
|
|
558
|
-
## End-to-end example
|
|
559
|
-
|
|
560
|
-
```ts
|
|
561
|
-
import {
|
|
562
|
-
importGedFile,
|
|
563
|
-
ExportGedzipFile,
|
|
564
|
-
editPerson,
|
|
565
|
-
DateAct,
|
|
566
|
-
Identifier,
|
|
567
|
-
} from "gedcom-ts";
|
|
568
|
-
|
|
569
|
-
async function importModifyExport(file: File) {
|
|
570
|
-
const readGed = await importGedFile(file);
|
|
571
|
-
const persons = readGed.persons;
|
|
572
|
-
|
|
573
|
-
if (persons.length > 0) {
|
|
574
|
-
editPerson(persons[0])
|
|
575
|
-
.setLastname(persons[0].lastname.toUpperCase())
|
|
576
|
-
.acts()
|
|
577
|
-
.at(0)
|
|
578
|
-
.setDateAct(new DateAct("1 JAN 1900"));
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
await new ExportGedzipFile("updated-tree", persons).download();
|
|
582
|
-
}
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
## Development
|
|
586
|
-
|
|
587
|
-
### Local scripts
|
|
588
|
-
|
|
589
|
-
| Script | Role |
|
|
590
|
-
| --- | --- |
|
|
591
|
-
| `npm run lint` | ESLint on the codebase |
|
|
592
|
-
| `npm run test` | Vitest test suite |
|
|
593
|
-
| `npm run build` | Production bundle + `.d.ts` |
|
|
594
|
-
| `npm run tgz` | `build` then `npm pack` (local `.tgz`) |
|
|
595
|
-
|
|
596
|
-
### GitLab CI (`.gitlab-ci.yml`)
|
|
597
|
-
|
|
598
|
-
Every pipeline runs **`InstallDependencies`** (`npm ci`, `node_modules` artifact) — required by all other jobs.
|
|
599
|
-
|
|
600
|
-
| Job | When it runs |
|
|
601
|
-
| --- | --- |
|
|
602
|
-
| `InstallDependencies` | Always |
|
|
603
|
-
| `Lint`, `Test` | Merge requests and branch pushes (not on `master` after a merge commit titled `Merge…`) |
|
|
604
|
-
| `BuildTgz`, `PublishNpm` | Push to `master` after merge only (`Merge…` commit title) |
|
|
605
|
-
|
|
606
|
-
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).
|
|
607
|
-
|
|
608
690
|
## Error handling
|
|
609
691
|
|
|
610
692
|
```ts
|
|
@@ -621,3 +703,4 @@ try {
|
|
|
621
703
|
}
|
|
622
704
|
}
|
|
623
705
|
```
|
|
706
|
+
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { normalizeCityKey, tidyCityDisplay, findCanonicalCityLabel, } from "./place-city-utils";
|
|
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, type SimilarCityActGroup, type GroupActsBySimilarCityOptions, type FilterActsBySimilarCityOptions, levenshteinDistance, citiesAreSimilar, clusterKeyForCity, similarCityKey, groupActsBySimilarCity, filterActsBySimilarCity, actsNeedingGeocodeForCity, findHarmonizationClustersFromActs, findHarmonizationClusters, applyCoordinatesToActs, applyGeocodeCandidateToActs, } from "./place-clusters";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Act } from "../commons/Act";
|
|
2
|
+
import type { ReadGed } from "../import/ReadGed";
|
|
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
|
+
*/
|
|
8
|
+
export declare function normalizeCityKey(city: string): string;
|
|
9
|
+
/** Libellé affiché : trim + espaces internes unifiés (sans forcer la casse). */
|
|
10
|
+
export declare function tidyCityDisplay(city: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Libellé canonique par **égalité stricte** {@link normalizeCityKey} (pas `citiesAreSimilar`).
|
|
13
|
+
* Pour une ville « proche », préférer le `cityLabel` d’un {@link groupActsBySimilarCity}.
|
|
14
|
+
*/
|
|
15
|
+
export declare function findCanonicalCityLabel(ged: ReadGed, cityInput: string, excludeAct: Act | null): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Act } from "../commons/Act";
|
|
2
|
+
import type { ReadGed } from "../import/ReadGed";
|
|
3
|
+
import { type GeocodeCandidate } from "./place-geocode";
|
|
4
|
+
export interface CoordVariant {
|
|
5
|
+
readonly lat: number;
|
|
6
|
+
readonly lng: number;
|
|
7
|
+
readonly actCount: number;
|
|
8
|
+
}
|
|
9
|
+
/** Groupe de lieux similaires à harmoniser (libellés ou coordonnées divergents). */
|
|
10
|
+
export interface CityHarmonizationCluster {
|
|
11
|
+
readonly clusterKey: string;
|
|
12
|
+
readonly labels: readonly string[];
|
|
13
|
+
readonly coordVariants: readonly CoordVariant[];
|
|
14
|
+
readonly acts: readonly Act[];
|
|
15
|
+
readonly actsWithoutCoord: number;
|
|
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
|
+
}
|
|
32
|
+
/** Distance de Levenshtein (petites chaînes de noms de villes). */
|
|
33
|
+
export declare function levenshteinDistance(a: string, b: string): number;
|
|
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
|
+
*/
|
|
38
|
+
export declare function citiesAreSimilar(a: string, b: string): boolean;
|
|
39
|
+
/**
|
|
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.
|
|
65
|
+
*/
|
|
66
|
+
export declare function findHarmonizationClustersFromActs(acts: readonly Act[]): CityHarmonizationCluster[];
|
|
67
|
+
/** Analyse toute l’arbre (préférer {@link findHarmonizationClustersFromActs} sur un sous-ensemble). */
|
|
68
|
+
export declare function findHarmonizationClusters(ged: ReadGed): CityHarmonizationCluster[];
|
|
69
|
+
export declare function applyCoordinatesToActs(acts: readonly Act[], lat: number, lng: number, patch?: {
|
|
70
|
+
city?: string;
|
|
71
|
+
country?: string | null;
|
|
72
|
+
}): void;
|
|
73
|
+
export declare function applyGeocodeCandidateToActs(acts: readonly Act[], candidate: GeocodeCandidate, preferredCityLabel?: string): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReadGed } from "../import/ReadGed";
|
|
2
|
+
/** Résultat Nominatim présenté à l’utilisateur. */
|
|
3
|
+
export interface GeocodeCandidate {
|
|
4
|
+
readonly label: string;
|
|
5
|
+
readonly shortLabel: string;
|
|
6
|
+
readonly lat: number;
|
|
7
|
+
readonly lng: number;
|
|
8
|
+
readonly country: string | null;
|
|
9
|
+
readonly countryCode: string | null;
|
|
10
|
+
readonly region: string | null;
|
|
11
|
+
readonly kind: string | null;
|
|
12
|
+
readonly importance: number;
|
|
13
|
+
}
|
|
14
|
+
export interface GeocodeContext {
|
|
15
|
+
/** Codes ISO 3166-1 alpha-2 (ex. `fr`), du plus au moins probable. */
|
|
16
|
+
readonly countryCodes: readonly string[];
|
|
17
|
+
readonly defaultCountryLabel: string | null;
|
|
18
|
+
readonly centroid: {
|
|
19
|
+
lat: number;
|
|
20
|
+
lng: number;
|
|
21
|
+
} | null;
|
|
22
|
+
}
|
|
23
|
+
export type GeocodeFetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
24
|
+
export interface GeocodeSearchOptions {
|
|
25
|
+
readonly countryCodes?: readonly string[];
|
|
26
|
+
readonly limit?: number;
|
|
27
|
+
/** Défaut : `gedcom-ts/<version> (genealogy library)` */
|
|
28
|
+
readonly userAgent?: string;
|
|
29
|
+
/** Client HTTP injectable (tests, Node sans fetch global). */
|
|
30
|
+
readonly fetchFn?: GeocodeFetchFn;
|
|
31
|
+
}
|
|
32
|
+
export declare const NOMINATIM_SEARCH_URL = "https://nominatim.openstreetmap.org/search";
|
|
33
|
+
export declare function defaultGeocodeUserAgent(): string;
|
|
34
|
+
/** Parse « Valence », « Valence, Drôme », « Valence, France ». */
|
|
35
|
+
export declare function parseCityQuery(raw: string): {
|
|
36
|
+
city: string;
|
|
37
|
+
countryHint: string | null;
|
|
38
|
+
};
|
|
39
|
+
export declare function countryLabelToIso(label: string): string | null;
|
|
40
|
+
export declare function isoToCountryLabel(code: string | null | undefined): string | null;
|
|
41
|
+
/** Contexte géographique déduit de l’arbre (pays et centroïde des lieux déjà géolocalisés). */
|
|
42
|
+
export declare function inferGeocodeContext(ged: ReadGed | null): GeocodeContext;
|
|
43
|
+
export declare function buildGeocodeQuery(cityInput: string, context: GeocodeContext, extraCountryHint?: string | null): string;
|
|
44
|
+
/** Priorise le pays saisi par l’utilisateur pour filtre Nominatim et classement. */
|
|
45
|
+
export declare function geocodeContextWithHint(context: GeocodeContext, extraCountryHint?: string | null): GeocodeContext;
|
|
46
|
+
/** Trie les candidats : pays de l’arbre, proximité du centroïde, correspondance du nom. */
|
|
47
|
+
export declare function rankGeocodeCandidates(candidates: GeocodeCandidate[], cityInput: string, context: GeocodeContext): GeocodeCandidate[];
|
|
48
|
+
export declare function searchPlaces(query: string, options?: GeocodeSearchOptions): Promise<GeocodeCandidate[]>;
|
|
49
|
+
/** Recherche avec repli sans filtre pays si trop peu de résultats. */
|
|
50
|
+
export declare function searchPlacesWithContext(cityInput: string, context: GeocodeContext, extraCountryHint?: string | null, options?: GeocodeSearchOptions): Promise<GeocodeCandidate[]>;
|