gedcom-ts
Version:
TypeScript GEDCOM toolkit for browser apps: import .ged/.zip, edit typed genealogy data, and export GEDCOM or GEDZIP.
964 lines (716 loc) • 49.6 kB
Markdown
# gedcom-ts
`gedcom-ts` is a browser-oriented TypeScript library to:
- import genealogy data from GEDCOM (`.ged`) or ZIP (`.zip` / `.gdz`)
- work with a typed JSON model (persons, acts, dates, places, notes, media, name variants, attributes)
- edit the model in-place through a fluent, chainable API (`editPerson`, `editAct`, …)
- export data back to GEDCOM (`.ged`) or GEDZIP (`.zip`)
- attach **audio** and other media via GEDCOM 7 `OBJE` records (`MEDI AUDIO`, multi-`FILE`, `TRAN`)
- model **oral history interviews** as `EVEN` acts with `OBJE` + transcription notes
- geocode event places (OpenStreetMap / Nominatim) and group similar city names
- generate a **genealogy booklet** as PDF (`gedcom-ts/booklet`, 11 locales including RTL Arabic/Hebrew)
## Live demo
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)**
## Contents
- [Installation](#installation)
- [Quick start](#quick-start)
- [Geocoding places](#geocoding-places)
- [Genealogy booklet (PDF)](#genealogy-booklet-pdf)
- [Audio and oral history (GEDCOM 7)](#audio-and-oral-history-gedcom-7)
- [API reference](#api-reference)
- [Error handling](#error-handling)
## Package
- NPM: [gedcom-ts](https://www.npmjs.com/package/gedcom-ts)
- Current release **`2026.6.4`** — affichage des prénoms GEDCOM 7 sans guillemets parasites après round-trip ; série **2026.6.x** (OBJE audio, GEDZIP fflate, dates insensibles à la casse). Version format **CalVer** `AAAA.M.micro` (e.g. `2026.6.4` = June 2026, micro release). See [CHANGELOG.md](CHANGELOG.md).
- Entry points:
- **`gedcom-ts`** — import, model, edit layer, export, geocoding
- **`gedcom-ts/booklet`** — PDF livret (`pdf-lib` en peerDependency, chargé depuis `node_modules`)
## Installation
```bash
npm install gedcom-ts
```
Both entry points come from the same package. For the PDF booklet, **`pdf-lib` is a peer dependency** — install it in your app if you use `gedcom-ts/booklet`:
```bash
npm install pdf-lib
```
For `zh` / `ar` / `he` PDF output, your bundler should also be able to resolve `@pdf-lib/fontkit` (included in `gedcom-ts` dependencies; the host app typically preloads it — see [Bundle layout](#bundle-layout) below).
## Runtime Requirements
- modern browser runtime (`File`, `Blob`, `XMLHttpRequest`, `URL.createObjectURL`)
- for pure Node.js usage, DOM polyfills are required (the library targets browsers)
## Quick start
```ts
import { importGedFile, ExportGedzipFile } from "gedcom-ts";
async function roundTrip(file: File) {
const readGed = await importGedFile(file);
const persons = readGed.persons;
if (persons.length > 0) {
persons[0].lastname = persons[0].lastname.toUpperCase();
}
await new ExportGedzipFile("updated-tree", persons).download();
}
```
Typical workflow:
1. import a file with `importGedFile` (or start from `createEmptyReadGed()`)
2. read / mutate the typed `Person`, `Act`, `Place`, `Note`, `MultimediaFile` objects (directly or through `editPerson` / `editAct` / `editPlace` / …)
3. export as `.ged` (`ExportGedcomFile`) or `.zip` (`ExportGedzipFile`)
## Geocoding places
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.
You work with a `ReadGed` (after import) and a flat `Act[]` built from every person’s events (see [Prepare act list](#prepare-act-list)).
### How city names are matched
The library groups variants of the same place with **`citiesAreSimilar`**, not exact string equality.
| Label A | Label B | Same place? |
| ----------- | ------------------------------------ | --------------------------- |
| `Pleurtuit` | `Pleurtuit, Ille-et-Vilaine, France` | Yes |
| `Paris` | `Paris, TX, USA` | Depends on similarity rules |
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).
### Prepare act list
```ts
import type { ReadGed, Act } from "gedcom-ts";
function collectAllActs(ged: ReadGed): Act[] {
const acts: Act[] = [];
for (const person of ged.persons) {
for (const act of person.acts.list) acts.push(act);
}
return acts;
}
```
### Geocode one city (typical flow)
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.
| Step | Function |
| ---------------------------------------------- | ----------------------------------------------------------- |
| 1. Context from the tree (countries, centroid) | `inferGeocodeContext(ged)` |
| 2. Search OpenStreetMap | `searchPlacesWithContext(cityName, context)` |
| 3. Acts to update | `actsNeedingGeocodeForCity(allActs, cityName)` |
| 4. Write coordinates | `applyGeocodeCandidateToActs(targets, candidate, cityName)` |
```ts
import {
inferGeocodeContext,
searchPlacesWithContext,
actsNeedingGeocodeForCity,
applyGeocodeCandidateToActs,
} from "gedcom-ts";
import type { ReadGed, Act } from "gedcom-ts";
async function geocodeCity(ged: ReadGed, allActs: Act[], cityName: string) {
const context = inferGeocodeContext(ged);
const candidates = await searchPlacesWithContext(cityName, context);
const chosen = candidates[0];
if (!chosen) return;
const targets = actsNeedingGeocodeForCity(allActs, cityName);
applyGeocodeCandidateToActs(targets, chosen, cityName);
}
```
> **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`.
### List cities missing coordinates
One row per city; `withoutCoordCount` is how many acts still need GPS.
```ts
import { groupActsBySimilarCity } from "gedcom-ts";
const groups = groupActsBySimilarCity(allActs, {
onlyWithoutCoordinates: true,
});
for (const group of groups) {
console.log(group.cityLabel, group.withoutCoordCount);
// group.acts — acts in this group without GPS
}
```
Largest groups first (`group.acts.length`).
### Map: one marker per city
```ts
import { clusterKeyForCity } from "gedcom-ts";
const markerKey = clusterKeyForCity(act.place?.city ?? "");
// merge markers that share the same markerKey
```
### Harmonization (data quality, optional)
Use when spellings, GPS positions, or “some acts with / without GPS” disagree for the same similar city.
```ts
import { findHarmonizationClusters } from "gedcom-ts";
for (const cluster of findHarmonizationClusters(ged)) {
console.log(cluster.labels, cluster.coordVariants, cluster.actsWithoutCoord);
}
```
After a full geocode via `actsNeedingGeocodeForCity`, you should not get a cluster that only reports missing GPS for that city.
### Geocoding API cheat sheet
| Goal | Call |
| ------------------------- | ---------------------------------------------------------------- |
| Search | `searchPlacesWithContext(city, inferGeocodeContext(ged))` |
| Acts to update on confirm | `actsNeedingGeocodeForCity(acts, city)` |
| Apply lat/lng | `applyGeocodeCandidateToActs(targets, candidate, city)` |
| Cities without GPS | `groupActsBySimilarCity(acts, { onlyWithoutCoordinates: true })` |
| Map marker id | `clusterKeyForCity(city)` |
| Inconsistencies | `findHarmonizationClusters(ged)` |
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.
The legacy callback `getCityCoordinates` is deprecated — use the flow above.
## Genealogy booklet (PDF)
The **`gedcom-ts/booklet`** subpath builds a printable family booklet: cover page, table of contents, chapters by generation (Sosa), family sheets, and narrative text from GEDCOM acts (birth, marriage, death, …). **English is the default locale** (`DEFAULT_BOOKLET_LOCALE`). Full copy is available for **`en`**, **`fr`**, **`de`**, **`nl`**, **`es`**, **`zh`**, **`it`**, **`pt`**, **`pl`**, **`ar`**, and **`he`** (aligned with [gedcomts.com](https://gedcomts.com) UI languages). Arabic and Hebrew use **RTL layout** in the PDF (`isBookletLocaleRtl`, `bookletTextDirection`).
### Bundle layout
Code is split so the host app only downloads what it uses. Approximate sizes after minify (obfuscation adds ~25–30 KiB on `gedcom-ts/booklet`).
| Artifact | Size (typ.) | When loaded |
| ------------------------------------------- | ----------: | -------------------------------------------- |
| `gedcom-ts` (`dist/index.mjs`) | ~180 KiB | GEDCOM import / edit / export |
| `gedcom-ts/booklet` (`dist/booklet.mjs`) | ~100 KiB | livret (collect, estimate, PDF) |
| `locale-chunks/booklet-locale-*` | 14–26 KiB | one UI language |
| `feature-chunks/booklet-logo-draw` | ~11 KiB | cover logo (`coverLogo !== false`) |
| `feature-chunks/booklet-timeline-raster` | ~3 KiB | `timelineStyle: "canvas"` |
| `font-chunks/booklet-pdf-font-bytes-ar` | ~43 KiB | Arabic PDF |
| `font-chunks/booklet-pdf-font-bytes-he` | ~13 KiB | Hebrew PDF |
| `font-chunks/booklet-pdf-font-bytes-zh` | ~190 KiB | Chinese PDF (base subset) |
| `font-chunks/booklet-pdf-font-bytes-zh-ext` | ~1.4 MiB | Chinese PDF when GEDCOM names need extra Han |
| `pdf-lib` (peer, not in gedcom-ts) | ~500 KiB+ | any PDF generation |
**Not bundled in gedcom-ts:** `fflate` (main entry, external), `pdf-lib` (booklet peer). **`bidi-js`** and **`naqqash`** (Arabic shaping / RTL reordering) ship inside `gedcom-ts/booklet` today.
Call **`await ensureBookletLocale(locale)`** before sync APIs. `generateGenealogyBookletPdf` preloads locale, logo, and timeline when needed.
**Lazy chunk subpaths** (for bundler preload or explicit `import()` — normal apps should use `ensureBookletLocale()` instead):
| Subpath | Role |
| ----------------------------------------------------------------------------------- | --------------------------------------------------- |
| `gedcom-ts/booklet/locale-chunks/booklet-locale-{en,fr,de,nl,es,zh,it,pt,pl,ar,he}` | Messages, narratives, date/place helpers per locale |
| `gedcom-ts/booklet/font-chunks/booklet-pdf-font-bytes-{ar,he,zh,zh-ext}` | Subset Noto font bytes |
| `gedcom-ts/booklet/feature-chunks/booklet-logo-draw` | Cover logo SVG paths |
| `gedcom-ts/booklet/feature-chunks/booklet-timeline-raster` | Canvas timeline PNG rasterizer |
Each subpath is listed in `package.json` **`exports`** with matching **`types`** (`.d.ts` under `dist/booklet/…`) and runtime (`.mjs` / `.cjs` under `dist/…`). TypeScript resolves them via `exports.types` (`moduleResolution: bundler` / `node16`). Adding a locale updates `scripts/booklet-chunk-manifest.mjs`; `npm run build:js` regenerates chunk files and syncs `exports`.
### Vite / Angular integration
1. **Lazy-load the booklet entry** — `import('gedcom-ts/booklet')` only when the user opens export / download (not on every tree page).
2. **Preload the UI locale** — `import('gedcom-ts/booklet/locale-chunks/booklet-locale-fr')` (or your loader) before sync APIs; then `ensureBookletLocale('fr')`.
3. **Unicode PDF (`zh` / `ar` / `he`)** — the host app should import font chunks explicitly and call `registerBookletPdfFontBytes` + `registerPdfFontkit` before `generateGenealogyBookletPdf` (see [gedcomts.com](https://gedcomts.com) `booklet-pdf-deps.loader.ts` for a reference pattern).
4. **Dev server** — exclude the main booklet bundle from Vite pre-bundling so lazy subpaths resolve via `node_modules`:
```ts
// vite.config.ts / angular.json (esbuild optimizeDeps)
optimizeDeps: {
exclude: ["gedcom-ts/booklet"],
},
```
Published `dist/booklet.mjs` keeps **literal** dynamic `import('gedcom-ts/booklet/…')` strings (rewrite before obfuscation, paths reserved from string encoding) so Vite can statically analyze chunk loads.
### Workflow
1. Import with `importGedFile` (`gedcom-ts`).
2. **`await ensureBookletLocale(locale)`** — load messages/narratives for the chosen language.
3. Collect persons with `collectBookletPersons` (`gedcom-ts/booklet`).
4. Optionally preview size with `estimateBookletSize` + `groupBookletIntoChapters`.
5. Build bytes with `generateGenealogyBookletPdf`, then `downloadBookletPdf` (browser).
```ts
import { importGedFile } from "gedcom-ts";
import {
collectBookletPersons,
ensureBookletLocale,
groupBookletIntoChapters,
estimateBookletSize,
generateGenealogyBookletPdf,
downloadBookletPdf,
personDisplayName,
} from "gedcom-ts/booklet";
async function exportBooklet(file: File) {
const ged = await importGedFile(file);
const root = ged.persons[0] ?? null;
const locale = "en" as const; // BookletLocale — align with your UI language (en, fr, de, nl, es, zh, it, pt, pl, ar, he)
await ensureBookletLocale(locale);
const entries = collectBookletPersons({
ged,
scope: "from-reference",
referencePerson: root,
maxGeneration: 6,
locale,
});
const chapters = groupBookletIntoChapters(entries, locale);
const families = chapters.reduce((n, ch) => n + ch.families.length, 0);
const size = estimateBookletSize(
chapters,
entries.length,
families,
"summary",
"canvas",
locale,
);
console.log(size.label);
const pdf = await generateGenealogyBookletPdf(
entries,
{
title: "Family booklet",
scopeLabel: "Ancestors and descendants",
personCount: entries.length,
referenceName: root ? personDisplayName(root) : undefined,
},
{
detailLevel: "summary",
timelineStyle: "canvas",
coverLogo: true,
locale,
},
);
downloadBookletPdf(pdf, "family-booklet.pdf");
}
```
Use `scope: "all"` and `referencePerson: null` to include every individual in the file.
### `collectBookletPersons` options
| Option | Role |
| ----------------- | --------------------------------------------------------------------------------------------------- |
| `ged` | `ReadGed` after import |
| `scope` | `"all"` or `"from-reference"` (Sosa from `referencePerson`) |
| `referencePerson` | Root person for `"from-reference"`; `null` if scope is `"all"` |
| `maxGeneration` | Max Sosa generation (e.g. `6`); ignored when `scope === "all"` |
| `locale` | `BookletLocale` (default `"en"`) — `en`, `fr`, `de`, `nl`, `es`, `zh`, `it`, `pt`, `pl`, `ar`, `he` |
Helpers: `personDisplayName`, `buildBookletPersonEntry`, `sortBookletEntries`, `bookletSexFromPerson`, `DEFAULT_BOOKLET_LOCALE`, `BOOKLET_LOCALES`, `isBookletLocaleRtl`.
### `generateGenealogyBookletPdf` options
| Option | Values | Effect |
| --------------- | ------------------------------------------------------- | ------------------------------------------------------ |
| `detailLevel` | `"summary"` \| `"detailed"` | Short prose per person vs longer biographies |
| `timelineStyle` | `"off"` \| `"canvas"` | Generation timeline pages per chapter |
| `coverLogo` | `true` (default), `false`, or `DrawGedcomTsLogoOptions` | gedcom-ts vector logo on the cover |
| `locale` | `BookletLocale` (default `"en"`) | Narrative, chapter titles, PDF chrome, timeline legend |
| `unicodeFont` | `Uint8Array` (optional) | Replace bundled Noto font for `zh` / `ar` / `he` |
`BookletPdfMeta`: `title`, `scopeLabel`, `personCount`, optional `referenceName` (usually translated in the host app).
### Cover logo
The cover draws the **gedcom-ts logo** from lazy-loaded SVG paths (`ensureBookletLogo`, `getGedcomTsLogoPaths`, `getGedcomTsLogoViewbox`). Reuse on custom PDF pages:
```ts
import { PDFDocument } from "pdf-lib";
import {
drawGedcomTsLogoOnPage,
defaultCoverLogoOptions,
ensureBookletLogo,
} from "gedcom-ts/booklet";
const doc = await PDFDocument.create();
const page = doc.addPage();
await ensureBookletLogo();
await drawGedcomTsLogoOnPage(page, defaultCoverLogoOptions());
```
### Booklet API cheat sheet
| Goal | Export |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Persons for the booklet | `collectBookletPersons` |
| Chapters / families | `groupBookletIntoChapters`, `partnerNamesLabel` |
| Localized narratives (custom UI) | `buildPersonSummaryNarrative`, `buildPersonDetailedNarratives`, `buildFamilyNarrative`, `buildChapterIntroNarrative` |
| Page estimate | `estimateBookletSize`, `bookletSizeAdvice` |
| Timeline data / PNG | `buildGenerationTimeline`, `rasterizeGenerationTimelinePng` |
| PDF output | `generateGenealogyBookletPdf`, `downloadBookletPdf`, `toPdfText`, `preparePdfText` |
| Locale helpers | `resolveBookletLocale`, `bookletLocaleToBcp47`, `bookletTextDirection`, `BOOKLET_RTL_LOCALES`, `ensureBookletLocale` |
| Font preload (host) | `registerBookletPdfFontBytes`, `registerPdfFontkit`, `localeNeedsUnicodePdfFont` |
| Logo | `ensureBookletLogo`, `drawGedcomTsLogoOnPage`, `getGedcomTsLogoPaths`, `defaultCoverLogoOptions` |
## Audio and oral history (GEDCOM 7)
Since **`2026.6.0`**, gedcom-ts models audio according to [FamilySearch GEDCOM 7](https://gedcom.io/): external files referenced by **`OBJE`** records (`FILE` + `FORM` + optional `MEDI AUDIO` + `TRAN` for alternate formats or WebVTT). Text transcriptions belong in **`NOTE`** under the event, not inside `OBJE`.
**Oral history pattern:** `1 EVEN` + `2 TYPE Interview` (or `Oral history`) + `2 OBJE @O…@` + `2 NOTE` (transcription).
```ts
import {
createAudioInterviewAct,
editAct,
ExportGedzipFile,
type Person,
type ReadGed,
} from "gedcom-ts";
async function addInterview(readGed: ReadGed, person: Person, mp3: File) {
const objeMap = new Map(readGed.objeRecordsById);
const { act, objeRecord } = createAudioInterviewAct(
{
description: "Entretien avec grand-mère",
participantIndis: [person.INDI],
ownerIndi: person.INDI,
audioFile: mp3,
transcription: "Bonjour, je m'appelle Marie…",
vttUri: "media/transcript.vtt", // optional FILE.TRAN on OBJE
},
readGed.persons,
objeMap,
);
person.acts.add(act);
if (objeRecord) {
objeMap.set(objeRecord.id, objeRecord);
}
await new ExportGedzipFile("my-tree", readGed.persons, {
extraTopLevelRecords: readGed.preservedTopLevelRecords,
objeRecordsById: objeMap,
}).download();
}
// enrich an existing act:
editAct(act)
.asAudioInterview(readGed.persons, objeMap)
.setTranscription("Suite de l'entretien…")
.attachAudio({
audioUri: "https://example.org/rec.mp3",
ownerIndi: person.INDI,
});
```
| Goal | API |
| -------------------------- | ------------------------------------------------------------------------ |
| Create interview act | `createAudioInterviewAct` |
| Attach audio to act | `attachAudioInterviewToAct`, `editAct(act).asAudioInterview()` |
| Detect imported interviews | `isAudioInterviewAct`, `isAudioInterviewEvenType` |
| Read transcription / media | `getAudioInterviewTranscription`, `primaryAudioInterviewUri` |
| OBJE round-trip | `readGed.objeRecordsById` → `objeRecordsById` in export options |
| Low-level OBJE parse/emit | `parseStandaloneObjeBlock`, `formatObjeRecordBlock`, `guessMediFromForm` |
| EVEN type constants | `GEDCOM_7_EVEN_TYPE_AUDIO_INTERVIEW`, `GEDCOM_7_EVEN_TYPE_ORAL_HISTORY` |
## API reference
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`).
### Importing a file
#### `importGedFile(file: File): Promise<ReadGed>`
Detects the format from the file MIME / extension and dispatches to the right reader.
- accepts a single `.ged` file
- accepts a `.zip` / `.gdz` archive containing one `.ged` + optional media files
- throws `Error(IMPORT_ERR_ZIP_GED_UNREADABLE)` when the embedded `.ged` cannot be decoded (typical cause: password-protected ZIP)
```ts
import { importGedFile } from "gedcom-ts";
const readGed = await importGedFile(fileInput.files![0]);
console.log(readGed.persons.length, readGed.datasetVersion);
```
#### `createEmptyReadGed(options?): ReadGed`
Creates an empty graph with the same runtime shape as a successful import (empty `persons`, initialized `mapPersons` / `partnersMap` / `childsMap` / `placesMap` / `mapFiles`). Use it to start a brand-new tree without parsing a file. `datasetVersion` is set to `"7.0"`.
```ts
import { createEmptyReadGed } from "gedcom-ts";
const readGed = createEmptyReadGed();
```
#### `IMPORT_ERR_ZIP_GED_UNREADABLE: string`
Sentinel error message thrown by `importGedFile` when a `.ged` inside a ZIP cannot be decoded (typically because the ZIP is password-protected). Compare with `error.message === IMPORT_ERR_ZIP_GED_UNREADABLE` to display a tailored message.
```ts
import { importGedFile, IMPORT_ERR_ZIP_GED_UNREADABLE } from "gedcom-ts";
try {
await importGedFile(file);
} catch (error) {
if (
error instanceof Error &&
error.message === IMPORT_ERR_ZIP_GED_UNREADABLE
) {
alert("Please unzip the archive manually and import the .ged file.");
}
}
```
#### `ReadGed`
Result of an import. Standalone `0 @O…@ OBJE` blocks with at least one `FILE` are parsed into **`objeRecordsById`** (multi-file, `MEDI`, `TRAN`). Other top-level records (`REPO`, `SOUR`, `SUBM`, …) stay on `preservedTopLevelRecords` for round-trip export.
Notable members:
| Member | Description |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `persons: Person[]` | Imported individuals. |
| `mapPersons: Map<number, Person>` | `INDI` (integer) → `Person`. |
| `partnersMap: Map<number, Person[]>` | Family id → spouses. |
| `childsMap: Map<number, Person[]>` | Family id → children. |
| `placesMap: Map<string, Place>` | City name → `Place` (first occurrence wins). |
| `mapFiles: Map<string, File>` | Relative path → media `File` (for ZIP imports). |
| `datasetVersion: GedcomDatasetVersion` | `"7.0"`, `"5.5"` or `"unknown"`. |
| `objeRecordsById: Map<number, ObjeRecord>` | Parsed `OBJE` records (audio, images, …) keyed by `@O{n}@`. |
| `preservedTopLevelRecords: string[]` | Raw blocks for partial round-trip. |
| `resolveIndividualPointer(raw)` | Resolves `@I12@`, `I12`, `@Homer_Simpson@`, `Homer_Simpson` to a `Person`. |
| `getChildrenForParent(parent)` | All children attached to a parent (via `FAMS`). |
| `getChildrenOfFamily(familyId)` | Children of a single family. |
| `groupPartners()` | Rebuilds `partnersMap` / `childsMap` after editing links. |
| `generateUniqueIndi()` | Next free `INDI` number for new persons. |
| `rehydratePlacesFromActs()` | Rebuilds `placesMap` from act places (e.g. after manual graph edits). Use `editReadGed(readGed).addPerson(...)` to register persons so maps stay coherent. |
```ts
import { ReadGed } from "gedcom-ts";
function describe(readGed: ReadGed) {
return {
version: readGed.datasetVersion,
persons: readGed.persons.length,
places: readGed.placesMap.size,
};
}
```
### Exporting
#### `ExportGedcomFile`
Writes a GEDCOM 7 (`.ged`) file. Three call signatures are supported:
```ts
new ExportGedcomFile(persons);
new ExportGedcomFile(title, persons);
new ExportGedcomFile(title, persons, options);
```
`.download()` triggers a browser download. `.toString()` returns the GEDCOM text.
```ts
import { ExportGedcomFile, type ReadGed } from "gedcom-ts";
function exportGed(readGed: ReadGed) {
new ExportGedcomFile("my-tree", readGed.persons, {
extraTopLevelRecords: readGed.preservedTopLevelRecords,
objeRecordsById: readGed.objeRecordsById,
headLanguageTag: "fr-FR",
headCopyright: "© 2026 Family Archive",
headDestination: "https://gedcom.io/",
headSchemaTagDefs: [{ tag: "_FOO", uri: "https://example.com/foo" }],
}).download();
}
```
#### `ExportGedzipFile`
Writes a `.zip` (GEDZIP) bundling the GEDCOM and all attached `MultimediaFile` payloads. Built with **fflate** (`zip` async, level 9).
```ts
import { ExportGedzipFile, Person } from "gedcom-ts";
async function exportZip(persons: Person[]) {
await new ExportGedzipFile("my-tree", persons).download();
}
async function exportZipBlob(persons: Person[]) {
return new ExportGedzipFile("my-tree", persons).gedzipBlob();
}
```
#### `GedcomExportOptions`
Options shared by both exporters:
| Option | Effect |
| ---------------------- | -------------------------------------------------------------------------------------------------------- |
| `extraTopLevelRecords` | Raw `0 …` blocks re-emitted before `SUBM` / `TRLR` (round-trip with `readGed.preservedTopLevelRecords`). |
| `objeRecordsById` | Full `OBJE` records (multi-`FILE`, `MEDI AUDIO`, `TRAN`) from `readGed.objeRecordsById`. |
| `headLanguageTag` | BCP 47 tag for `HEAD`.`LANG` (defaults to `en-US`). |
| `headCopyright` | One-line `1 COPR` notice. |
| `headDestination` | Value of `HEAD`.`DEST` (target app / URI). |
| `headSchemaTagDefs` | Extension tag definitions emitted as `HEAD`.`SCHMA` / `2 TAG …`. |
### Domain model
#### `Person`, `Sex`
```ts
import { Person, Sex } from "gedcom-ts";
const person = new Person();
person.INDI = 1;
person.SEX = Sex.M; // M | F | U | X
person.firstnames = ["Jean"];
person.lastname = "DUPONT";
person.addMultimedia(/* MultimediaFile */);
person.deleteMultimedia("1/photo.jpg");
```
Key fields: `INDI`, `sosa`, `SEX`, `firstnames`, `lastname`, `FAMC`, `FAMS`, `acts`, `notes`, `multimediaFiles`, `nameVariants`, `attributes`.
#### `PersonGedcomImportOptions` (type)
Optional hints consumed by `Person.createPersonJson` when re-parsing a single individual block (label-keyed pointers for `FAMS` / `FAMC` / `NOTE`, and standalone `OBJE` payloads). Useful when assembling a graph manually outside `ReadGed`.
#### `PersonNameVariant`, `PersonNameTranslation`
GEDCOM 7 may quote the preferred given name in `1 NAME` (e.g. `"Jean" /Dupont/`). Since **2026.6.4**, `personDisplayName`, `personInitialsFromPerson`, and `formatPersonNameFromVariant` strip those quotes for UI display after import or export round-trip.
Lossless representation of every `1 NAME` block of an individual (type, `NPFX`/`GIVN`/`SURN`/… parts, `TRAN` translations). `Person.nameVariants` keeps them in order so alias / translation data survive an import → export round-trip.
```ts
import { PersonNameVariant, PersonNameTranslation } from "gedcom-ts";
const variant = new PersonNameVariant("Jean /Dupont/", "BIRTH");
variant.parts.push({ level: "2", tag: "GIVN", value: "Jean" });
variant.translations.push(new PersonNameTranslation("ジャン /デュポン/", "jp"));
```
#### `IndiAttribute`, `IndiGedcomSubLine`
Generic level-1 individual attributes (`FACT`, `DSCR`, `CAST`, `EDUC`, `OCCU`, `RELI`, `TITL`, `RESN`, …) with their sub-lines preserved (`IndiGedcomSubLine = { level; tag; value }`).
```ts
import { IndiAttribute } from "gedcom-ts";
const occ = new IndiAttribute("OCCU", "Blacksmith", [
{ level: "2", tag: "DATE", value: "1820" },
]);
```
#### `Act`, `Acts`, `TypeAct`, `ActConstructionOptions`
`Act` models an individual or family event (BIRT, MARR, etc.). `Acts` is the ordered collection on a `Person`. `TypeAct` is the union of every supported GEDCOM 7 event tag (= `Gedcom7EventTag`). Event-level notes live on `act.notes` (`2 NOTE` / `3 CONT` in GEDCOM); they are distinct from `person.notes` (`1 NOTE` on `INDI`) and round-trip through ZIP export/import.
```ts
import { Act, Acts, Identifier, type TypeAct } from "gedcom-ts";
const acts = new Acts();
const type: TypeAct = Identifier.BIRT;
acts.add(new Act(type));
acts.sortByDate();
```
`ActConstructionOptions` controls the extras when building an `Act` manually:
```ts
import { Act, DateAct, Identifier } from "gedcom-ts";
const act = new Act(
Identifier.EVEN,
new DateAct("12 JAN 1901"),
null,
null,
null,
undefined,
undefined,
{
evenDescription: "Won a medal",
evenTypeLabel: "Award",
sdateAct: new DateAct("13 JAN 1901"),
eventPhrases: ["family gathering"],
preservedSubrecordPrefix: [],
preservedSubrecordSuffix: [],
},
);
```
#### `GEDCOM_7_ALL_EVENT_TAGS`, `GEDCOM_7_EVENT_SORT_ORDER`, `GEDCOM_7_EVENT_TAG_SET`, `GEDCOM_7_PAIR_UNION_EVENT_TAGS`, `Gedcom7EventTag`, `Gedcom7PairUnionEventTag`
Canonical lists of GEDCOM 7 event tags (`INDIVIDUAL_EVENT_STRUCTURE` ∪ `FAMILY_EVENT_STRUCTURE`, LDS ordinances excluded). Use them to build UI selects or guard custom logic. `GEDCOM_7_PAIR_UNION_EVENT_TAGS` lists tags commonly used when creating a family union via `createMarriageFamily` (`ENGA`, `MARB`, `MARC`, `MARL`, `MARR`, `MARS`).
```ts
import {
GEDCOM_7_ALL_EVENT_TAGS,
GEDCOM_7_EVENT_SORT_ORDER,
GEDCOM_7_EVENT_TAG_SET,
type Gedcom7EventTag,
} from "gedcom-ts";
const options: Gedcom7EventTag[] = [...GEDCOM_7_ALL_EVENT_TAGS];
const isEvent = GEDCOM_7_EVENT_TAG_SET.has("MARR");
const sorted = [...GEDCOM_7_EVENT_SORT_ORDER];
```
#### `DateAct`, `Day`, `Month`, `dateToDateLine`, `days`, `months`, `TypeDateActSimpleQualifier`
GEDCOM dates with full GEDCOM 7 / 5.5 support, including `INT`, `EST`, `CAL`, `BET … AND …`, `FROM … TO …`, ISO `YYYY-MM-DD`, `3 PHRASE` under `DATE`, `3 TIME`, and a verbatim fallback for unparseable payloads. Month tokens (`JAN`, `may`, …) and simple qualifiers are matched **case-insensitively** (since **2026.6.2**).
```ts
import { DateAct, Day, Month, dateToDateLine, days, months } from "gedcom-ts";
const day: Day = 12;
const month: Month = months[0]; // JAN
const dateLine = dateToDateLine(1901, month, day); // "12 JAN 1901"
const dateAct = new DateAct(dateLine);
const formattedDate = dateAct.date;
const knownDaysCount = days.length;
```
`TypeDateActSimpleQualifier` is the union of single-anchor qualifiers (`BEF | ABT | AFT | INT | EST | CAL`) accepted by `DateAct.updateQualifiedDate` and by `DateActEdit.setQualified`.
#### `Place`, `CoordinateGPS`
```ts
import { Place, CoordinateGPS } from "gedcom-ts";
const place = new Place("Paris, FR", new CoordinateGPS(48.8566, 2.3522));
place.setFromGedcom7PlacPayload("Paris, Île-de-France, France");
const payload = place.toGedcom7PlacPayload(); // "Paris, , Île-de-France, France"
```
`Place` understands the `City, County, State, Country` GEDCOM 7 list (1 to 4+ segments) and exposes `placPhrase` for `3 PHRASE` under `PLAC` (import + export).
#### `MultimediaFile`, `MultimediaFiles`
Wraps either a local `File`, an external URI (`sourceUri`) or an OBJE pointer (`objeXrefId`) so the same model can survive a GED / ZIP round-trip. See [Audio and oral history](#audio-and-oral-history-gedcom-7) for interview workflows.
```ts
import { MultimediaFile, MultimediaFiles } from "gedcom-ts";
const bucket = new MultimediaFiles();
bucket.relativePath = "1/BIRT";
bucket.add(new MultimediaFile(new File(["img"], "birth.jpg")));
const remote = new MultimediaFile(undefined, "https://example.com/p.jpg");
const isExportable = remote.hasExportablePayload();
```
#### `Note`, `Notes`, `TypeNote`
```ts
import { Notes, Note, Identifier, type TypeNote } from "gedcom-ts";
const typeNote: TypeNote = Identifier.CONT;
const note = new Note();
note.updateType(typeNote);
note.updateLines(["first line", "second line"]);
const notes = new Notes();
notes.addNote(note);
notes.removeFromIndex(0);
```
#### `Identifier` (and deprecated `Identificator`)
Enum of every GEDCOM tag the library refers to (`INDI`, `BIRT`, `MARR`, `DATE`, `PLAC`, …). Prefer `Identifier`; `Identificator` is kept as a deprecated alias.
```ts
import { Identifier } from "gedcom-ts";
const birthTag = Identifier.BIRT;
```
#### `EventsByYears`, `ActsByYear`
Group acts by year, deduplicating per individual. Handy to build chronological timelines.
```ts
import { EventsByYears, ActsByYear } from "gedcom-ts";
const grouped = new EventsByYears(person.acts.list);
for (const bucket of grouped.events) {
console.log(bucket.year, bucket.list.length);
}
const yearBucket = new ActsByYear(1901);
```
### Edit layer (in-place, chainable)
Wrap an existing model object to mutate it with a fluent API. Every method returns `this`, so you can chain. `.value` exposes the underlying target.
```ts
import { editPerson, editDateAct, DateAct } from "gedcom-ts";
editPerson(person)
.setLastname("Dupont")
.setFirstnames(["Jean", "Marie"])
.acts()
.at(0)
.setDateAct(new DateAct("1 JAN 1900"));
editDateAct(person.acts.list[0].dateAct!).setExactDate(1900, "JAN", 1);
```
| Helper / class | Purpose |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `editPerson(person)` / `PersonEdit` | Identity, `FAMS` / `FAMC` / `removeFams*`, `nameVariants()` / `attributes()` / `multimedia()`, bulk `clear*`, entry points to `acts()` and `notes()`. |
| `editActs(acts)` / `ActsEdit` | `add`, `addNew`, `insertAt`, `insertNewAt`, `replaceAt`, `removeAt`, `removeAct`, `removeLast`, `clear`, `sortByDate`, `at`, `indexOfAct`. |
| `editAct(act)` / `ActEdit` | `setType`, `setIndis`, dates, place, EVEN fields, preserved lines, `notes()`, `multimedia()`, `asAudioInterview()`, `clearNotes`, `clearMultimedia`. |
| `editAudioInterviewAct(act)` / `AudioInterviewActEdit` | Oral-history facade: `attachAudio`, `setTranscription`, `setDescription`, `registerPendingObje`, `isAudioInterview`, … |
| `editDateAct(dateAct)` / `DateActEdit` | `clear`, `applyGedcomPayload`, `setExactDate`, `setQualified`, `setBetween`, `setFromTo`, `setTime`, `setDatePhrase` / `appendDatePhrase`, `setVerbatimPayload`. |
| `editPlace(place)` / `PlaceEdit` | `setFromGedcom7Payload`, `setCity`, `setCounty`, `setState`, `setCountry`, `setPlacPhrase` / `appendPlacPhrase` / `clearPlacPhrase`, `setCoordinates` / `clearCoordinates` / `replaceCoordinateModel`, `clearStructured`. |
| `editNotes(notes)` / `NotesEdit` | `add`, `addNew`, `insertAt`, `insertNewAt`, `replaceAt`, `removeAt`, `removeNote`, `removeLast`, `clear`, `at`, `indexOfNote`. |
### Dataset editing: `ReadGed`, graph, clone, export options, validation
Higher-level helpers complement the per-object edit facades:
```ts
import type { GedcomExportOptions } from "gedcom-ts";
import {
createEmptyReadGed,
createPersonStub,
DateAct,
Sex,
editReadGed,
editGedcomExportOptions,
clonePerson,
cloneAct,
createMarriageFamily,
linkChildToFamily,
removeFamilyReferencesFromDataset,
addPersonToReadGed,
validateReadGed,
Identifier,
Act,
} from "gedcom-ts";
const readGed = createEmptyReadGed();
const p = createPersonStub(readGed.generateUniqueIndi(), {
sex: Sex.M,
firstnames: ["Jean"],
lastname: "Dupont",
});
editReadGed(readGed).addPerson(p);
p.acts.add(new Act(Identifier.BIRT, new DateAct("1900")));
editReadGed(readGed).preserved().append("0 @S42@ SOUR Custom");
const opts: GedcomExportOptions = {};
editGedcomExportOptions(opts)
.setHeadCopyright("© 2026")
.setExtraTopLevelRecords([...readGed.preservedTopLevelRecords]);
const twin = clonePerson(p, readGed.generateUniqueIndi());
const birthCopy = cloneAct(p.acts.list[0]!);
// const fam = createMarriageFamily(readGed, spouseA, spouseB, { dateAct: new DateAct("1 JAN 2000") }, { eventTag: Identifier.ENGA });
// linkChildToFamily(readGed, child, fam);
// removeFamilyReferencesFromDataset(readGed, fam);
validateReadGed(readGed, {
checkMarrParticipants: true,
checkFamcWithoutSpouses: true,
checkFamsWithoutSpouses: true,
checkDuplicateFamsEntries: true,
checkAncestorCycles: true,
});
```
#### Commands (invariants before mutation)
`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`.
| Export | Role |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `editReadGed(readGed)` / `ReadGedEdit` | `addPerson`, `removePersonByIndi`, `preserved()` → `PreservedTopLevelEdit` (`append`, `insertAt`, `replaceAt`, `removeAt`, `clear`) on `preservedTopLevelRecords`. Low-level helpers: `addPersonToReadGed`, `removePersonFromReadGedByIndi`. |
| `editGedcomExportOptions(opts)` / `GedcomExportOptionsEdit` | Fluent setters for `extraTopLevelRecords`, `objeRecordsById`, `headLanguageTag`, `headCopyright`, `headDestination`, `headSchemaTagDefs`. |
| `clonePerson(person, newIndi)` / `cloneAct(act)` / `person.clone` / `act.clone` | Deep copies for templates or undo stacks. |
| `nextFamilyId(persons)` | Next internal family id `F`. |
| `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`. |
| `validateReadGed`, `assertReadGedConsistent`, `validatePerson`, `assertPersonConsistent` | Typed `code` / `severity` (`error` \| `warn`). Options: `checkMarrParticipants`, `checkDuplicateIndis`, `checkFamcWithoutSpouses`, `checkFamsWithoutSpouses`, `checkDuplicateFamsEntries`, `checkAncestorCycles`. Assertions: `failOn` (default: `error`). |
| `tryAddPersonToReadGed`, `tryRemovePersonFromReadGedByIndi`, `tryLinkChildToFamily`, `tryUnlinkChildFromFamily`, `tryCreateMarriageFamily`, `commandBlockingIssues` | Command layer with `CommandResult` / `validate*Command` prechecks. |
### Utilities
#### `createSosaMap(root, partnersMap)`
Computes a Sosa numbering (Ahnentafel) starting at `root` (Sosa 1) and recursively walking ancestors via `partnersMap`.
```ts
import { createSosaMap, Person } from "gedcom-ts";
const sosaMap = createSosaMap(root, readGed.partnersMap);
// e.g. sosaMap.get(root.INDI) === 1
```
#### `remainingTypesAct(acts, actToUpdate?)`
Returns the list of event types still allowed for a person’s `Acts`, enforcing the “unique per individual” rule for `BIRT` / `DEAT` / `BURI` / `CHR` while keeping every other tag selectable. Pass the currently edited act as `actToUpdate` so its own type stays in the list when re-opening a form.
```ts
import { remainingTypesAct, Acts } from "gedcom-ts";
const available = remainingTypesAct(new Acts());
```
#### `getCityCoordinates(cityName, callback)` (deprecated)
Legacy callback API. Use [Geocoding places](#geocoding-places) instead.
#### `resolveDatasetVersion(headerLines)` / `GedcomDatasetVersion`
Inspects the lines of a `0 HEAD` block and returns `"7.0"`, `"5.5"` or `"unknown"`. Useful to branch UI behaviour for legacy datasets.
```ts
import { resolveDatasetVersion } from "gedcom-ts";
const version = resolveDatasetVersion(headLines); // "7.0" | "5.5" | "unknown"
```
#### `guessMediaFormFromUri(uri)`
Best-effort media type detection from a path/URI or local `File` (`file.type` when set). Covers images (incl. **tiff**, **heic**, svg), **audio** (mp3, **webm**, ogg, wav, m4a, flac, …), **video** (mp4, mov, mkv, …), subtitles (vtt, srt), and documents (pdf, djvu, docx, odt, xlsx, csv, html). Used to fill `OBJE`.`FILE`.`FORM`. Pair with `guessMediFromForm` for `MEDI` (`AUDIO`, `PHOTO`, `VIDEO`, `ELECTRONIC`, …). `refineGenericObjeForm` upgrades legacy `application/octet-stream` exports using the file extension.
```ts
import { guessMediaFormFromUri, guessMediaFormFromFile } from "gedcom-ts";
guessMediaFormFromUri("media/recording.webm"); // "audio/webm"
guessMediaFormFromFile(new File(["x"], "take.webm", { type: "audio/webm" })); // "audio/webm"
```
#### `extractPersonNameVariants(personLines)` / `extractIndiAttributes(personLines)`
Low-level parsers used by `Person.createPersonJson`. They turn the raw GEDCOM lines of a single `INDI` record into structured `PersonNameVariant[]` / `IndiAttribute[]`. Reuse them when parsing custom GEDCOM fragments outside `ReadGed`.
```ts
import { extractPersonNameVariants, extractIndiAttributes } from "gedcom-ts";
const variants = extractPersonNameVariants(rawIndiLines);
const attributes = extractIndiAttributes(rawIndiLines);
```
#### `selectPrimaryNameVariant(variants)`
Picks the most relevant `1 NAME` block: priority to `2 TYPE BIRTH`, then to a name with a `/surname/` payload, otherwise the first variant.
```ts
import { selectPrimaryNameVariant } from "gedcom-ts";
const primary = selectPrimaryNameVariant(person.nameVariants);
```
#### `GEDCOM_LIBRARY_VERSION`
CalVer string written in exported `HEAD`.`SOUR`.`VERS` (same value as the npm package version, e.g. `2026.6.4`).
```ts
import { GEDCOM_LIBRARY_VERSION } from "gedcom-ts";
console.log(`gedcom-ts ${GEDCOM_LIBRARY_VERSION}`);
```
## Error handling
```ts
import { importGedFile, IMPORT_ERR_ZIP_GED_UNREADABLE } from "gedcom-ts";
try {
const readGed = await importGedFile(file);
console.log(readGed.persons.length);
} catch (error) {
if (
error instanceof Error &&
error.message === IMPORT_ERR_ZIP_GED_UNREADABLE
) {
console.error("Encrypted ZIP: please extract the .ged manually.");
} else {
console.error("GED import failed:", error);
}
}
```