@mailwoman/registry 4.8.1

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.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Export resolved entities as GeoJSON — the bridge to QGIS and any web map. Each entity becomes a
7
+ * Point at its resolved coordinate, carrying the properties an analyst needs: how many records
8
+ * merged into it, how tightly (cohesion), the canonical name/org/address, and the geocode tier
9
+ * (so rooftop-precise entities can be styled apart from admin-centroid guesses). Entities without
10
+ * a coordinate are omitted (a Point feature needs one).
11
+ */
12
+ import type { GeoJsonFeatureCollection, ResolvedEntity } from "./types.js";
13
+ /**
14
+ * Convert resolved entities into a GeoJSON `FeatureCollection` of points, ready for QGIS. Entities
15
+ * with no resolved coordinate are skipped.
16
+ */
17
+ export declare function toGeoJSON(entities: readonly ResolvedEntity[]): GeoJsonFeatureCollection;
18
+ //# sourceMappingURL=geojson.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geojson.d.ts","sourceRoot":"","sources":["../geojson.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAkB,wBAAwB,EAAE,cAAc,EAAgB,MAAM,YAAY,CAAA;AAmCxG;;;GAGG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,wBAAwB,CAKvF"}
package/out/geojson.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Export resolved entities as GeoJSON — the bridge to QGIS and any web map. Each entity becomes a
7
+ * Point at its resolved coordinate, carrying the properties an analyst needs: how many records
8
+ * merged into it, how tightly (cohesion), the canonical name/org/address, and the geocode tier
9
+ * (so rooftop-precise entities can be styled apart from admin-centroid guesses). Entities without
10
+ * a coordinate are omitted (a Point feature needs one).
11
+ */
12
+ /** Assemble a display name from a record's parsed person name, if any. */
13
+ function displayName(record) {
14
+ const name = record.name;
15
+ if (!name)
16
+ return null;
17
+ const joined = [name.prefix, name.given, name.middle, name.familyParticle, name.family, name.suffix]
18
+ .filter(Boolean)
19
+ .join(" ")
20
+ .trim();
21
+ return joined || null;
22
+ }
23
+ /** One entity → one GeoJSON Point feature. */
24
+ function toFeature(entity) {
25
+ const rep = entity.representative;
26
+ return {
27
+ type: "Feature",
28
+ geometry: {
29
+ type: "Point",
30
+ coordinates: [entity.coordinate.longitude, entity.coordinate.latitude],
31
+ },
32
+ properties: {
33
+ entityId: entity.id,
34
+ recordCount: entity.records.length,
35
+ cohesion: entity.cohesion,
36
+ sourceIds: entity.records.map((r) => r.id),
37
+ name: displayName(rep),
38
+ organization: rep.organization?.canonical ?? null,
39
+ address: rep.address?.formatted ?? null,
40
+ geocodeTier: rep.address?.geocode?.tier ?? null,
41
+ },
42
+ };
43
+ }
44
+ /**
45
+ * Convert resolved entities into a GeoJSON `FeatureCollection` of points, ready for QGIS. Entities
46
+ * with no resolved coordinate are skipped.
47
+ */
48
+ export function toGeoJSON(entities) {
49
+ return {
50
+ type: "FeatureCollection",
51
+ features: entities.filter((entity) => entity.coordinate).map(toFeature),
52
+ };
53
+ }
54
+ //# sourceMappingURL=geojson.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geojson.js","sourceRoot":"","sources":["../geojson.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,0EAA0E;AAC1E,SAAS,WAAW,CAAC,MAAoB;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;IACxB,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACtB,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;SAClG,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC;SACT,IAAI,EAAE,CAAA;IACR,OAAO,MAAM,IAAI,IAAI,CAAA;AACtB,CAAC;AAED,8CAA8C;AAC9C,SAAS,SAAS,CAAC,MAAsB;IACxC,MAAM,GAAG,GAAG,MAAM,CAAC,cAAc,CAAA;IACjC,OAAO;QACN,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE;YACT,IAAI,EAAE,OAAO;YACb,WAAW,EAAE,CAAC,MAAM,CAAC,UAAW,CAAC,SAAS,EAAE,MAAM,CAAC,UAAW,CAAC,QAAQ,CAAC;SACxE;QACD,UAAU,EAAE;YACX,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM;YAClC,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1C,IAAI,EAAE,WAAW,CAAC,GAAG,CAAC;YACtB,YAAY,EAAE,GAAG,CAAC,YAAY,EAAE,SAAS,IAAI,IAAI;YACjD,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,SAAS,IAAI,IAAI;YACvC,WAAW,EAAE,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,IAAI,IAAI;SAC/C;KACD,CAAA;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,QAAmC;IAC5D,OAAO;QACN,IAAI,EAAE,mBAAmB;QACzB,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC;KACvE,CAAA;AACF,CAAC"}
package/out/index.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/registry` — the geocode-first record-matching application.
7
+ *
8
+ * {@link resolveEntities} runs the whole matcher (block → score → cluster) over normalized
9
+ * contact/organization {@link SourceRecord}s and returns canonical {@link ResolvedEntity entities};
10
+ * {@link toGeoJSON} exports them for QGIS. This is the clinic-funding use case mailwoman was built
11
+ * for, finally standing on a calibrated, label-free matcher.
12
+ */
13
+ export * from "./geojson.js";
14
+ export * from "./ingest.js";
15
+ export * from "./resolve.js";
16
+ export * from "./types.js";
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA"}
package/out/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/registry` — the geocode-first record-matching application.
7
+ *
8
+ * {@link resolveEntities} runs the whole matcher (block → score → cluster) over normalized
9
+ * contact/organization {@link SourceRecord}s and returns canonical {@link ResolvedEntity entities};
10
+ * {@link toGeoJSON} exports them for QGIS. This is the clinic-funding use case mailwoman was built
11
+ * for, finally standing on a calibrated, label-free matcher.
12
+ */
13
+ export * from "./geojson.js";
14
+ export * from "./ingest.js";
15
+ export * from "./resolve.js";
16
+ export * from "./types.js";
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Ingest — turn messy tabular data (CSV, SQLite, hand-keyed spreadsheets) into normalized
7
+ * {@link SourceRecord}s, the front of the cascade.
8
+ *
9
+ * Two concerns, kept separate:
10
+ *
11
+ * 1. **Column mapping + normalization** (this is pure): a {@link ColumnMapping} says which column(s)
12
+ * hold the name, organization, address, phone, email; each row is normalized with
13
+ * `@mailwoman/record` (parse the person name, canonicalize the org). This is deterministic
14
+ * and testable with no heavy runtime.
15
+ * 2. **Geocoding** (the heavy part) is an injected seam — a {@link GeocodeAddress} the caller provides.
16
+ * Ingest never imports the neural parser, the resolver, or the shards; it just calls the
17
+ * seam per address. {@link geocodeAddressVia} builds that seam from mailwoman's real parse +
18
+ * geocode primitives (which the CLI constructs with the model + data in hand), so the wiring
19
+ * is concrete and testable without pinning the heavy runtime into this package.
20
+ *
21
+ * LLM-assisted column mapping (infer the mapping from a header + samples) is a documented
22
+ * fast-follow; the mapping is an explicit input here.
23
+ */
24
+ import type { AddressGeocode, PostalAddress } from "@mailwoman/record";
25
+ import { toPostalAddress } from "@mailwoman/record";
26
+ import type { SourceRecord } from "./types.js";
27
+ /** Resolve a raw address string into a {@link PostalAddress}. The seam to mailwoman's geocoder. */
28
+ export type GeocodeAddress = (raw: string) => Promise<PostalAddress | null> | PostalAddress | null;
29
+ /** Maps dataset columns to record fields. A field may draw from several columns (joined with
30
+ spaces). */
31
+ export interface ColumnMapping {
32
+ /** Column holding a stable row id. Falls back to the row index. */
33
+ id?: string;
34
+ /** A literal provenance label for every row (not a column). */
35
+ source?: string;
36
+ name?: string | string[];
37
+ organization?: string | string[];
38
+ address?: string | string[];
39
+ phone?: string;
40
+ email?: string;
41
+ }
42
+ /** Options for {@link ingestRows}. */
43
+ export interface IngestOptions {
44
+ /** The geocoding seam. Without it, records carry name/org but no resolved address. */
45
+ geocodeAddress?: GeocodeAddress;
46
+ }
47
+ /** Parse a CSV string (with a header row) into row objects keyed by column name. */
48
+ export declare function parseCsv(text: string): Record<string, string>[];
49
+ /** Normalize tabular rows into {@link SourceRecord}s under a {@link ColumnMapping}. */
50
+ export declare function ingestRows(rows: Iterable<Record<string, string>>, mapping: ColumnMapping, opts?: IngestOptions): Promise<SourceRecord[]>;
51
+ /**
52
+ * The subset of mailwoman's `GeocodeResult` the adapter consumes — kept structural so this package
53
+ * never imports the heavy geocoder, yet a real `GeocodeResult` maps straight in.
54
+ */
55
+ export interface RawGeocode {
56
+ lat: number | null;
57
+ lon: number | null;
58
+ resolution_tier: AddressGeocode["tier"];
59
+ uncertainty_m: number | null;
60
+ hierarchy?: AddressGeocode["hierarchy"];
61
+ }
62
+ /**
63
+ * Build a {@link GeocodeAddress} from mailwoman's real parse + geocode primitives (injected — the
64
+ * CLI constructs the neural parser, resolver, and shards and passes them in). Parse → components →
65
+ * {@link toPostalAddress} (which fills the canonical key + formatted form) → attach the resolved
66
+ * coordinate. When geocoding can't place the address, the parsed-but-unlocated address is still
67
+ * returned.
68
+ */
69
+ export declare function geocodeAddressVia(deps: {
70
+ parse: (raw: string) => Promise<Parameters<typeof toPostalAddress>[0]> | Parameters<typeof toPostalAddress>[0];
71
+ geocode: (raw: string) => Promise<RawGeocode | null> | RawGeocode | null;
72
+ country?: string;
73
+ }): GeocodeAddress;
74
+ //# sourceMappingURL=ingest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../ingest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtE,OAAO,EAAiD,eAAe,EAAe,MAAM,mBAAmB,CAAA;AAE/G,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,mGAAmG;AACnG,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,GAAG,aAAa,GAAG,IAAI,CAAA;AAElG;WACW;AACX,MAAM,WAAW,aAAa;IAC7B,mEAAmE;IACnE,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAChC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACd;AAED,sCAAsC;AACtC,MAAM,WAAW,aAAa;IAC7B,sFAAsF;IACtF,cAAc,CAAC,EAAE,cAAc,CAAA;CAC/B;AAED,oFAAoF;AACpF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAE/D;AAcD,uFAAuF;AACvF,wBAAsB,UAAU,CAC/B,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACtC,OAAO,EAAE,aAAa,EACtB,IAAI,GAAE,aAAkB,GACtB,OAAO,CAAC,YAAY,EAAE,CAAC,CA2BzB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IAC1B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAA;IACvC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,CAAC,EAAE,cAAc,CAAC,WAAW,CAAC,CAAA;CACvC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACvC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC,CAAA;IAC9G,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,UAAU,GAAG,IAAI,CAAA;IACxE,OAAO,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,cAAc,CAgBjB"}
package/out/ingest.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Ingest — turn messy tabular data (CSV, SQLite, hand-keyed spreadsheets) into normalized
7
+ * {@link SourceRecord}s, the front of the cascade.
8
+ *
9
+ * Two concerns, kept separate:
10
+ *
11
+ * 1. **Column mapping + normalization** (this is pure): a {@link ColumnMapping} says which column(s)
12
+ * hold the name, organization, address, phone, email; each row is normalized with
13
+ * `@mailwoman/record` (parse the person name, canonicalize the org). This is deterministic
14
+ * and testable with no heavy runtime.
15
+ * 2. **Geocoding** (the heavy part) is an injected seam — a {@link GeocodeAddress} the caller provides.
16
+ * Ingest never imports the neural parser, the resolver, or the shards; it just calls the
17
+ * seam per address. {@link geocodeAddressVia} builds that seam from mailwoman's real parse +
18
+ * geocode primitives (which the CLI constructs with the model + data in hand), so the wiring
19
+ * is concrete and testable without pinning the heavy runtime into this package.
20
+ *
21
+ * LLM-assisted column mapping (infer the mapping from a header + samples) is a documented
22
+ * fast-follow; the mapping is an explicit input here.
23
+ */
24
+ import { canonicalizeOrganizationName, parsePersonName, toPostalAddress, withGeocode } from "@mailwoman/record";
25
+ import { parse as parseCsvSync } from "csv-parse/sync";
26
+ /** Parse a CSV string (with a header row) into row objects keyed by column name. */
27
+ export function parseCsv(text) {
28
+ return parseCsvSync(text, { columns: true, skip_empty_lines: true, trim: true, relax_column_count: true });
29
+ }
30
+ /** Join the named column(s) of a row into a single trimmed string, or undefined if empty. */
31
+ function pick(row, columns) {
32
+ if (!columns)
33
+ return undefined;
34
+ const list = Array.isArray(columns) ? columns : [columns];
35
+ const value = list
36
+ .map((c) => row[c]?.trim())
37
+ .filter(Boolean)
38
+ .join(" ")
39
+ .trim();
40
+ return value || undefined;
41
+ }
42
+ /** Normalize tabular rows into {@link SourceRecord}s under a {@link ColumnMapping}. */
43
+ export async function ingestRows(rows, mapping, opts = {}) {
44
+ const records = [];
45
+ let index = 0;
46
+ for (const row of rows) {
47
+ const id = (mapping.id ? row[mapping.id]?.trim() : "") || String(index);
48
+ const nameValue = pick(row, mapping.name);
49
+ const orgValue = pick(row, mapping.organization);
50
+ const addressValue = pick(row, mapping.address);
51
+ const record = {
52
+ id,
53
+ source: mapping.source,
54
+ name: nameValue ? parsePersonName(nameValue) : undefined,
55
+ organization: orgValue ? canonicalizeOrganizationName(orgValue) : undefined,
56
+ phone: (mapping.phone && row[mapping.phone]?.trim()) || undefined,
57
+ email: (mapping.email && row[mapping.email]?.trim()?.toLowerCase()) || undefined,
58
+ address: addressValue && opts.geocodeAddress ? ((await opts.geocodeAddress(addressValue)) ?? undefined) : undefined,
59
+ raw: row,
60
+ };
61
+ records.push(record);
62
+ index++;
63
+ }
64
+ return records;
65
+ }
66
+ /**
67
+ * Build a {@link GeocodeAddress} from mailwoman's real parse + geocode primitives (injected — the
68
+ * CLI constructs the neural parser, resolver, and shards and passes them in). Parse → components →
69
+ * {@link toPostalAddress} (which fills the canonical key + formatted form) → attach the resolved
70
+ * coordinate. When geocoding can't place the address, the parsed-but-unlocated address is still
71
+ * returned.
72
+ */
73
+ export function geocodeAddressVia(deps) {
74
+ return async (raw) => {
75
+ const components = await deps.parse(raw);
76
+ const base = toPostalAddress(components, { country: deps.country, raw });
77
+ const resolved = await deps.geocode(raw);
78
+ if (!resolved || resolved.lat === null || resolved.lon === null)
79
+ return base;
80
+ const geocode = {
81
+ coordinate: { latitude: resolved.lat, longitude: resolved.lon },
82
+ tier: resolved.resolution_tier,
83
+ uncertaintyMeters: resolved.uncertainty_m,
84
+ hierarchy: resolved.hierarchy,
85
+ };
86
+ return withGeocode(base, geocode);
87
+ };
88
+ }
89
+ //# sourceMappingURL=ingest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.js","sourceRoot":"","sources":["../ingest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The resolve pipeline — the whole matcher, wired over concrete contact/organization records: block
7
+ * (geo-first) → score (Fellegi-Sunter) → cluster → canonical entities.
8
+ *
9
+ * `resolveEntities` ships sensible geocode-first defaults — block on location / canonical key /
10
+ * phone / email; score on name, organization, address key, and great-circle distance — and can
11
+ * fit the scorer's `m`/`u` to the data with EM (`trainEM`), so it runs with no labels and no
12
+ * per-dataset tuning. Everything is overridable: pass your own model, blocking keys, or
13
+ * threshold.
14
+ *
15
+ * Geocoding is assumed already done upstream (each `address` carries its coordinate + canonical
16
+ * key). Wiring mailwoman's parser + geocoder to turn raw rows into `SourceRecord`s is the ingest
17
+ * layer that sits in front of this.
18
+ */
19
+ import { type BlockingKey, type FellegiSunterModel } from "@mailwoman/match";
20
+ import type { ResolvedEntity, SourceRecord } from "./types.js";
21
+ /**
22
+ * The default geocode-first scoring model: name + organization + address key + great-circle
23
+ * distance.
24
+ */
25
+ export declare function buildDefaultModel(): FellegiSunterModel<SourceRecord>;
26
+ /** The default blocking keys: a union of location, canonical address, phone, and email. */
27
+ export declare function defaultBlockingKeys(): BlockingKey<SourceRecord>[];
28
+ /** Options for {@link resolveEntities}. */
29
+ export interface ResolveConfig {
30
+ /** Scoring model. Default {@link buildDefaultModel}. */
31
+ model?: FellegiSunterModel<SourceRecord>;
32
+ /** Blocking keys (their union). Default {@link defaultBlockingKeys}. */
33
+ blockingKeys?: BlockingKey<SourceRecord>[];
34
+ /** Link two records into the same entity at or above this match weight (bits). Default 0. */
35
+ threshold?: number;
36
+ /** Skip and report blocks larger than this rather than scanning them. */
37
+ maxBlockSize?: number;
38
+ /**
39
+ * Fit the model's `m`/`u` to the candidate pairs with EM before scoring (label-free). Default
40
+ * false.
41
+ */
42
+ trainEM?: boolean;
43
+ }
44
+ /** The outcome of a resolve pass. */
45
+ export interface ResolveResult {
46
+ entities: ResolvedEntity[];
47
+ /** Number of candidate pairs blocking produced. */
48
+ candidatePairs: number;
49
+ /** Blocks too large to scan, surfaced so coverage limits are visible. */
50
+ droppedBlocks: Array<{
51
+ key: string;
52
+ size: number;
53
+ }>;
54
+ }
55
+ /**
56
+ * Resolve source records into canonical entities: block → score → cluster. Every record lands in
57
+ * exactly one entity (a record with no confident link is its own singleton entity).
58
+ */
59
+ export declare function resolveEntities(records: readonly SourceRecord[], config?: ResolveConfig): ResolveResult;
60
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,kBAAkB,EAavB,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAS9D;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,kBAAkB,CAAC,YAAY,CAAC,CAepE;AAED,2FAA2F;AAC3F,wBAAgB,mBAAmB,IAAI,WAAW,CAAC,YAAY,CAAC,EAAE,CAOjE;AAED,2CAA2C;AAC3C,MAAM,WAAW,aAAa;IAC7B,wDAAwD;IACxD,KAAK,CAAC,EAAE,kBAAkB,CAAC,YAAY,CAAC,CAAA;IACxC,wEAAwE;IACxE,YAAY,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,EAAE,CAAA;IAC1C,6FAA6F;IAC7F,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,qCAAqC;AACrC,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAA;IACtB,yEAAyE;IACzE,aAAa,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACnD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,EAAE,MAAM,GAAE,aAAkB,GAAG,aAAa,CAsC3G"}
package/out/resolve.js ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The resolve pipeline — the whole matcher, wired over concrete contact/organization records: block
7
+ * (geo-first) → score (Fellegi-Sunter) → cluster → canonical entities.
8
+ *
9
+ * `resolveEntities` ships sensible geocode-first defaults — block on location / canonical key /
10
+ * phone / email; score on name, organization, address key, and great-circle distance — and can
11
+ * fit the scorer's `m`/`u` to the data with EM (`trainEM`), so it runs with no labels and no
12
+ * per-dataset tuning. Everything is overridable: pass your own model, blocking keys, or
13
+ * threshold.
14
+ *
15
+ * Geocoding is assumed already done upstream (each `address` carries its coordinate + canonical
16
+ * key). Wiring mailwoman's parser + geocoder to turn raw rows into `SourceRecord`s is the ingest
17
+ * layer that sits in front of this.
18
+ */
19
+ import { DEFAULT_DISTANCE_LEVELS, agreementPattern, block, cluster, distanceComparison, estimateParameters, exactKey, geoCellKey, representative, scorePair, similarityComparison, } from "@mailwoman/match";
20
+ /** Default tiered levels for a name-like text field. `m`/`u` are EM-estimable seeds. */
21
+ const NAME_LEVELS = [
22
+ { label: "exact", minSimilarity: 1.0, m: 0.8, u: 0.01 },
23
+ { label: "high", minSimilarity: 0.88, m: 0.15, u: 0.03 },
24
+ { label: "different", minSimilarity: 0, m: 0.05, u: 0.96 },
25
+ ];
26
+ /**
27
+ * The default geocode-first scoring model: name + organization + address key + great-circle
28
+ * distance.
29
+ */
30
+ export function buildDefaultModel() {
31
+ return {
32
+ lambda: 0.0001,
33
+ comparisons: [
34
+ similarityComparison({ name: "given", extract: (r) => r.name?.given, levels: NAME_LEVELS }),
35
+ similarityComparison({ name: "family", extract: (r) => r.name?.family, levels: NAME_LEVELS }),
36
+ similarityComparison({ name: "organization", extract: (r) => r.organization?.canonical, levels: NAME_LEVELS }),
37
+ similarityComparison({ name: "address", extract: (r) => r.address?.canonicalKey, levels: NAME_LEVELS }),
38
+ distanceComparison({
39
+ name: "distance",
40
+ extract: (r) => r.address?.geocode?.coordinate,
41
+ levels: DEFAULT_DISTANCE_LEVELS,
42
+ }),
43
+ ],
44
+ };
45
+ }
46
+ /** The default blocking keys: a union of location, canonical address, phone, and email. */
47
+ export function defaultBlockingKeys() {
48
+ return [
49
+ geoCellKey((r) => r.address?.geocode?.coordinate),
50
+ exactKey((r) => r.address?.canonicalKey),
51
+ exactKey((r) => r.phone),
52
+ exactKey((r) => r.email),
53
+ ];
54
+ }
55
+ /**
56
+ * Resolve source records into canonical entities: block → score → cluster. Every record lands in
57
+ * exactly one entity (a record with no confident link is its own singleton entity).
58
+ */
59
+ export function resolveEntities(records, config = {}) {
60
+ const model = config.model ?? buildDefaultModel();
61
+ const blockingKeys = config.blockingKeys ?? defaultBlockingKeys();
62
+ const threshold = config.threshold ?? 0;
63
+ const { pairs, droppedBlocks } = block(records, blockingKeys, { maxBlockSize: config.maxBlockSize });
64
+ let scoringModel = model;
65
+ if (config.trainEM && pairs.length > 0) {
66
+ const patterns = pairs.map(([a, b]) => agreementPattern(model.comparisons, a, b));
67
+ scoringModel = estimateParameters(model, patterns).model;
68
+ }
69
+ const links = pairs.map(([a, b]) => ({
70
+ a,
71
+ b,
72
+ weight: scorePair(scoringModel, a, b).weight,
73
+ }));
74
+ const clusters = cluster(records, links, { threshold });
75
+ const entities = clusters.map((group, i) => {
76
+ const members = new Set(group);
77
+ const intraWeights = links
78
+ .filter((link) => link.weight >= threshold && members.has(link.a) && members.has(link.b))
79
+ .map((link) => link.weight);
80
+ const rep = representative(group) ?? group[0];
81
+ return {
82
+ id: `entity-${i}`,
83
+ records: group,
84
+ representative: rep,
85
+ coordinate: rep.address?.geocode?.coordinate ?? undefined,
86
+ cohesion: group.length > 1 && intraWeights.length > 0 ? Math.min(...intraWeights) : null,
87
+ };
88
+ });
89
+ return { entities, candidatePairs: pairs.length, droppedBlocks };
90
+ }
91
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../resolve.ts"],"names":[],"mappings":"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"}
package/out/types.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The application's data shapes — the messy record that goes in, and the canonical entity that
7
+ * comes out. Plain interfaces over the `@mailwoman/record` types.
8
+ */
9
+ import type { OrganizationName, PersonName, PostalAddress } from "@mailwoman/record";
10
+ /** A single source record: one row of a messy contact/organization dataset, after normalization. */
11
+ export interface SourceRecord {
12
+ /** Stable identifier within the input (row id, primary key…). */
13
+ id: string;
14
+ /** Provenance: which file / dataset this came from. */
15
+ source?: string | null;
16
+ /** Parsed person name, when the record is a contact. */
17
+ name?: PersonName | null;
18
+ /** Canonicalized organization name, when the record is an org. */
19
+ organization?: OrganizationName | null;
20
+ /** The address, normalized + (ideally) geocoded. */
21
+ address?: PostalAddress | null;
22
+ phone?: string | null;
23
+ email?: string | null;
24
+ /** The original row, verbatim, for audit. */
25
+ raw?: Record<string, string>;
26
+ }
27
+ /** A resolved canonical entity: a cluster of source records judged to be the same real-world thing. */
28
+ export interface ResolvedEntity {
29
+ /** Identifier assigned to the entity. */
30
+ id: string;
31
+ /** The source records that resolved to this entity. */
32
+ records: SourceRecord[];
33
+ /** The most complete record, used as the entity's canonical face. */
34
+ representative: SourceRecord;
35
+ /** The entity's location, from the representative's geocode. */
36
+ coordinate?: {
37
+ latitude: number;
38
+ longitude: number;
39
+ };
40
+ /**
41
+ * Weakest within-cluster link weight in bits (how tightly it holds together); `null` for a
42
+ * singleton.
43
+ */
44
+ cohesion: number | null;
45
+ }
46
+ export interface GeoJsonPoint {
47
+ type: "Point";
48
+ /** `[longitude, latitude]`, per the GeoJSON spec. */
49
+ coordinates: [number, number];
50
+ }
51
+ export interface GeoJsonFeature {
52
+ type: "Feature";
53
+ geometry: GeoJsonPoint;
54
+ properties: Record<string, unknown>;
55
+ }
56
+ export interface GeoJsonFeatureCollection {
57
+ type: "FeatureCollection";
58
+ features: GeoJsonFeature[];
59
+ }
60
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEpF,oGAAoG;AACpG,MAAM,WAAW,YAAY;IAC5B,iEAAiE;IACjE,EAAE,EAAE,MAAM,CAAA;IACV,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,wDAAwD;IACxD,IAAI,CAAC,EAAE,UAAU,GAAG,IAAI,CAAA;IACxB,kEAAkE;IAClE,YAAY,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACtC,oDAAoD;IACpD,OAAO,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,6CAA6C;IAC7C,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC5B;AAED,uGAAuG;AACvG,MAAM,WAAW,cAAc;IAC9B,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAA;IACV,uDAAuD;IACvD,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,qEAAqE;IACrE,cAAc,EAAE,YAAY,CAAA;IAC5B,gEAAgE;IAChE,UAAU,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IACpD;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAID,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,OAAO,CAAA;IACb,qDAAqD;IACrD,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC7B;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,SAAS,CAAA;IACf,QAAQ,EAAE,YAAY,CAAA;IACtB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,mBAAmB,CAAA;IACzB,QAAQ,EAAE,cAAc,EAAE,CAAA;CAC1B"}
package/out/types.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The application's data shapes — the messy record that goes in, and the canonical entity that
7
+ * comes out. Plain interfaces over the `@mailwoman/record` types.
8
+ */
9
+ export {};
10
+ //#endregion
11
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;;AA0DH,YAAY"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@mailwoman/registry",
3
+ "version": "4.8.1",
4
+ "description": "The geocode-first record-matching application: resolve messy contact/organization records into canonical, geocoded entities (block → score → cluster) and export them as GeoJSON for spatial analysis. The clinic-funding use case mailwoman was built for.",
5
+ "license": "AGPL-3.0-only",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/sister-software/mailwoman.git",
9
+ "directory": "registry"
10
+ },
11
+ "type": "module",
12
+ "exports": {
13
+ "./package.json": "./package.json",
14
+ ".": "./out/index.js",
15
+ "./resolve": "./out/resolve.js",
16
+ "./geojson": "./out/geojson.js",
17
+ "./ingest": "./out/ingest.js"
18
+ },
19
+ "dependencies": {
20
+ "@mailwoman/match": "4.8.1",
21
+ "@mailwoman/record": "4.8.1",
22
+ "csv-parse": "^5.6.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^25.9.2"
26
+ },
27
+ "files": [
28
+ "out/**/*.js",
29
+ "out/**/*.js.map",
30
+ "out/**/*.d.ts",
31
+ "out/**/*.d.ts.map"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }