Kinship labels for a 426 person tree
2026-09-07
Connector lines said two people were joined but not how. Naming the relation in three languages cost 645ms per canvas until both graph walks were cached.
The archive is a self-hosted family tree running as a single Cloudflare Worker with one D1 database and one R2 bucket. Version 214 already put a kinship word on every card, outline row and person panel, but it rendered only for a viewer who had claimed a person in the tree, and no member had claimed one, so in practice a connector line still said two people were joined and not how. Versions 215 through 222 made the seat resolvable and gave the words their range: your father, your paternal uncle, your second cousin. The seat resolves in three tiers. A signed-in member who has claimed a person in the tree wins; a password visitor can seat themselves in browser-local state, which matters because most of this family has no account; with no account and no claim, whoever the reader last selected becomes the seat, which is what makes the feature visible to a stranger on the public demo. lib/relationship-path.ts computes the relation and now also reports which parent a blood line climbs through, so an uncle carries his side. lib/kinship-words.ts renders the result in English, Persian and French, where the distinction is not cosmetic: a paternal uncle and a maternal uncle are different words.
Version 221 named the in-laws instead of falling back to a generic phrase. Five relations are named directly (son and daughter-in-law, father and mother-in-law, brother and sister-in-law, step-parent, step-child), and everyone else is described through the marriage, as in your paternal uncle's wife or your husband's nephew. Persian picks the word by which family was married into, so a man's mother-in-law and a woman's mother-in-law are two different terms. On the live 426-person tree that gave 130 people a real word for their connection and left five on the generic phrase. It also cost a great deal: naming an in-law asks a second relationship question through each marriage bridge, and labelling one canvas went from 19ms to 645ms. The fix was to cache both graph walks per source person inside relationshipIndex, the ancestor walk and the breadth-first predecessor sweep. One sweep from a seat reaches everybody, so labelling hundreds of cards is one sweep rather than hundreds. The whole tree came back to 37ms with the in-laws named. The code comment records the reason the sweep does not stop at its target: the next question starts from the same person anyway.
Two related pieces landed on the same graph. Ninety-three percent of this archive's people have no birth date, so every dated view simply omitted them. lib/estimated-dates.ts places the undated from their relatives' recorded years using a 28-year generation, with siblings and spouses treated as close, propagated through the graph with a widening spread. Estimates are computed on read and never written to a record, and every consumer marks them, so the timeline went from 48 entries to 446 and each new one reads c. 1930 with the relatives it rests on. lib/who-can-answer.ts then ranks what the archive is missing, 398 birth dates and 408 birth places, by the number of steps to the nearest living relative, on the reasoning that nobody alive can say where a man born in 1780 was born. 421 of 422 incomplete records turned out to have a living relative within reach. The weekly digest became one letter per member carrying three questions about records near that member's own seat, widening the search radius until it finds three worth asking.