@bicharts/shape-core 0.1.7 → 0.1.8
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/dist/index.mjs +12 -3
- package/dist/index.mjs.map +2 -2
- package/dist/types/geoPointRoles.d.ts +15 -4
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -79,7 +79,11 @@ function distinctNormalized(values) {
|
|
|
79
79
|
function looksLikeCountryColumn(values) {
|
|
80
80
|
const d = distinctNormalized(values);
|
|
81
81
|
if (d.length === 0) return false;
|
|
82
|
-
|
|
82
|
+
if (!d.every((v) => COUNTRY_IDS.has(v))) return false;
|
|
83
|
+
return d.some((v) => !resolveAdmin1(v));
|
|
84
|
+
}
|
|
85
|
+
function isAmbiguousCountryState(normalized) {
|
|
86
|
+
return COUNTRY_IDS.has(normalized) && !!resolveAdmin1(normalized);
|
|
83
87
|
}
|
|
84
88
|
function admin1MatchPct(values) {
|
|
85
89
|
const d = distinctNormalized(values);
|
|
@@ -138,7 +142,10 @@ function resolvePointRoles(columns, rows, hint) {
|
|
|
138
142
|
const hintedState = hint?.state;
|
|
139
143
|
if (hintedState && colSet.has(hintedState)) {
|
|
140
144
|
const vals = valuesOf(hintedState);
|
|
141
|
-
|
|
145
|
+
const dvals = distinctNormalized(vals);
|
|
146
|
+
if (dvals.length > 0 && dvals.every(isAmbiguousCountryState)) {
|
|
147
|
+
refused.push(`state=${hintedState} (every value is "CA", which is both Canada and California)`);
|
|
148
|
+
} else if (looksLikeCountryColumn(vals)) {
|
|
142
149
|
refused.push(`state=${hintedState} (every value is a country, not a state)`);
|
|
143
150
|
if (!out.country) out.country = hintedState;
|
|
144
151
|
} else if (admin1MatchPct(vals) < THRESHOLD_PCT) {
|
|
@@ -156,7 +163,9 @@ function resolvePointRoles(columns, rows, hint) {
|
|
|
156
163
|
if (looksLikeCountryColumn(vals)) continue;
|
|
157
164
|
const pct = admin1MatchPct(vals);
|
|
158
165
|
if (pct < THRESHOLD_PCT) continue;
|
|
159
|
-
|
|
166
|
+
const hits = distinctMatches(vals, (v) => !!resolveAdmin1(v));
|
|
167
|
+
const unambiguous = distinctMatches(vals, (v) => !!resolveAdmin1(v) && !isAmbiguousCountryState(v));
|
|
168
|
+
if (hits < MIN_DISTINCT_BACKFILL && unambiguous < 1) continue;
|
|
160
169
|
if (!best || pct > best.pct) best = { name: c, pct };
|
|
161
170
|
}
|
|
162
171
|
if (best) {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/geoPointRoles.ts"],
|
|
4
|
-
"sourcesContent": ["// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// POINT-COLUMN ROLE RESOLUTION \u2014 which column is the city, the state, the ZIP\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// geoPoint.ts answers \"given a city/state/zip, WHERE is this row?\". This module answers\n// the question that comes first: \"which COLUMN is the city, and which is the state?\".\n//\n// That used to be answered by the model: the codegen response names pointCityColumn /\n// pointStateColumn / pointZipColumn and the host geocoded from whatever it was handed.\n// A production North America point map showed why that is not sufficient. The model was\n// City{distinct=23}, StateCode{distinct=16}, Country{distinct=4}, <2 measures>\n// and answered `{\"city\":\"City\"}` \u2014 no state. Geocoding then ran city-only, so the\n// City+State tier never engaged, the \"state contradicts city\" guard never had a state\n// to check, and 8 of 32 marks were placed by the largest-match tie-break on a bare\n// name (\"Burlington\" -> Burlington ONTARIO, not Vermont) with the map presenting them\n// as ordinary points. Nothing was broken; a field was simply left blank.\n//\n// These roles are DETERMINISTICALLY RESOLVABLE from the values, and every classifier\n// needed already exists in geoPoint.ts. So the model's answer is treated as a HINT that\n// gets VERIFIED, and any role it omitted is BACKFILLED from the data. A hint is still\n// useful \u2014 it disambiguates which of two plausible columns was intended \u2014 but it is\n// never the only thing standing between the user and a wrong coordinate.\n//\n// THE COUNTRY TRAP this also closes. `resolveAdmin1(\"CA\")` is CALIFORNIA. Measured\n// across 15 country identifiers (\"US\" \"USA\" \"United States\" \"CAN\" \"Canada\" \"MX\" \"MEX\"\n// \"Mexico\" \u2026) that is the ONLY collision \u2014 but it is a catastrophic one: a Country\n// column bound into the state slot makes every Canadian city fail to match inside\n// California and fall back to the California centroid, piling Mississauga, Montr\u00E9al,\n// Burnaby and Laval onto one dot outside Bakersfield. \"Canada, inside the USA.\"\n//\n// It cannot be fixed the way the sibling \"Mexico\" collision was (geoPoint.ts drops the\n// bare admin1 NAME \"mexico\" because the country reading dominates and the State of\n// M\u00E9xico is obscure). For \"CA\" the California reading is overwhelmingly the common one\n// in real US data, so deleting the key would break far more than it fixes. The fix has\n// to be CONTEXTUAL, which is exactly what a column-level pass can do and a row-level\n// lookup cannot: judge the column by ALL its values at once.\n\nimport { normalizePlaceName, resolveAdmin1, isKnownCity, zipPrefixCandidates } from \"./geoPoint\";\n\n/** The place parts a coordinate can be resolved from. Any subset. */\nexport type PointBind = {\n city?: string;\n state?: string;\n zip?: string;\n lat?: string;\n lon?: string;\n /** Narrows a city name to the row's own country. Never inferred from a value in\n * another slot \u2014 see the \"CA\" note above. */\n country?: string;\n};\n\nexport type PointRoleResolution = {\n /** The binding to geocode with: hint, minus anything refused, plus anything backfilled. */\n bind: PointBind | null;\n /** Roles the hint omitted that were resolved from the data (\"state=StateCode\"). */\n backfilled: string[];\n /** Roles the hint named that were REFUSED, with why (\"state=Country (country column)\"). */\n refused: string[];\n};\n\n// Share of DISTINCT values a column must match before a role is claimed for it. Mirrors\n// geoDetector's THRESHOLD_PCT: high enough that a column of something else cannot claim\n// a role by accident, loose enough to tolerate the blanks and typos real data carries.\nconst THRESHOLD_PCT = 85;\n// A role BACKFILLED from the data (no hint) additionally needs this many distinct\n// matches \u2014 geoDetector's roster guard. One matching value is a coincidence, not a\n// column: a lone \"CA\" is far likelier to be Canada than a single-state California table.\nconst MIN_DISTINCT_BACKFILL = 2;\n// ZIP is held to a HIGHER bar than the name roles, because its classifier is pure digit\n// shape and measures are digits too: a Revenue column carrying 54300 / 48900 / 33700\n// reads as a perfectly good run of 5-digit ZIPs. Callers are expected to keep measures\n// out of the candidate list, but that is one `isMeasure` flag away from failing open, and\n// a measure silently adopted as the ZIP column would relocate every point on the map. A\n// real ZIP column is ALL ZIPs, so demand exactly that.\nconst ZIP_THRESHOLD_PCT = 100;\n// Classification reads DISTINCT values, so a wide column costs no more than a narrow\n// one; this only bounds a pathological all-unique text column.\nconst MAX_DISTINCT_SCAN = 400;\n\n// Country identifiers, normalized. Deliberately NOT a general ISO table: this exists\n// only to break the one measured admin1 collision, and a column of countries is\n// recognized by ALL its values being country identifiers, not by any single one.\nconst COUNTRY_IDS = new Set([\n \"us\", \"usa\", \"u s\", \"u s a\", \"united states\", \"united states of america\", \"america\",\n \"ca\", \"can\", \"canada\",\n \"mx\", \"mex\", \"mexico\", \"estados unidos mexicanos\",\n]);\n\n/** Distinct, normalized, non-blank values of a column (capped). */\nfunction distinctNormalized(values: Array<unknown>): string[] {\n const seen = new Set<string>();\n for (const v of values) {\n if (v === null || v === undefined) continue;\n const k = normalizePlaceName(String(v));\n if (!k) continue;\n if (!seen.has(k)) {\n seen.add(k);\n if (seen.size >= MAX_DISTINCT_SCAN) break;\n }\n }\n return Array.from(seen);\n}\n\n/**\n * Is this column a list of COUNTRIES?\n *\n * True only when EVERY distinct value is a country identifier. That is the precise\n * shape of the trap: {US, CA, MX} and {CA} are countries, while {CA, TX} is a US state\n * column that happens to contain California. Being strict here matters \u2014 a loose test\n * would start refusing legitimate state columns.\n */\nexport function looksLikeCountryColumn(values: Array<unknown>): boolean {\n const d = distinctNormalized(values);\n if (d.length === 0) return false;\n return d.every(v => COUNTRY_IDS.has(v));\n}\n\n/** Share of DISTINCT values that resolve to a state/province, 0..100 (one decimal). */\nexport function admin1MatchPct(values: Array<unknown>): number {\n const d = distinctNormalized(values);\n if (d.length === 0) return 0;\n let n = 0;\n for (const v of d) if (resolveAdmin1(v)) n++;\n return Math.round((n / d.length) * 1000) / 10;\n}\n\n/** Distinct RAW (trimmed) values \u2014 for ZIPs, which normalizePlaceName would mangle:\n * it rewrites punctuation to spaces, turning \"90210-1234\" into \"90210 1234\", which\n * zipPrefixCandidates' ZIP+4 pattern then rejects. */\nfunction distinctRaw(values: Array<unknown>): string[] {\n const seen = new Set<string>();\n for (const v of values) {\n if (v === null || v === undefined) continue;\n const s = String(v).trim();\n if (!s) continue;\n if (!seen.has(s)) {\n seen.add(s);\n if (seen.size >= MAX_DISTINCT_SCAN) break;\n }\n }\n return Array.from(seen);\n}\n\n/** Share of DISTINCT values that read as a ZIP / ZIP-prefix, 0..100 (one decimal). */\nexport function zipMatchPct(values: Array<unknown>): number {\n const d = distinctRaw(values);\n if (d.length === 0) return 0;\n let n = 0;\n for (const v of d) if (zipPrefixCandidates(v).length > 0) n++;\n return Math.round((n / d.length) * 1000) / 10;\n}\n\n/** Share of DISTINCT values that are known city names, 0..100 (one decimal). */\nfunction cityPct(values: Array<unknown>): number {\n const d = distinctNormalized(values);\n if (d.length === 0) return 0;\n let n = 0;\n for (const v of d) if (isKnownCity(v)) n++;\n return Math.round((n / d.length) * 1000) / 10;\n}\n\nfunction distinctMatches(values: Array<unknown>, pred: (v: string) => boolean): number {\n let n = 0;\n for (const v of distinctNormalized(values)) if (pred(v)) n++;\n return n;\n}\n\n/** distinctMatches over RAW values (ZIP only \u2014 see distinctRaw). */\nfunction distinctRawMatches(values: Array<unknown>, pred: (v: string) => boolean): number {\n let n = 0;\n for (const v of distinctRaw(values)) if (pred(v)) n++;\n return n;\n}\n\n/**\n * Verify the hinted point-column roles against the data and fill in what the hint left\n * out. Pure: `columns` are the available column names, `rows` the row objects keyed by\n * those names, `hint` whatever the codegen response named (may be null).\n *\n * `columns` MUST exclude measures. A place is a dimension; a measure that wandered into\n * this list can be adopted as the ZIP column on digit shape alone (see ZIP_THRESHOLD_PCT)\n * and would move every point on the map.\n *\n * A role is only ever REFUSED for a positive reason (the column is countries; the column\n * does not look like states at all) \u2014 never merely because the classifier is unsure. And\n * a refusal drops that ONE role, leaving the rest of the binding intact, because\n * geocoding from city alone still beats geocoding from nothing.\n */\nexport function resolvePointRoles(\n columns: string[],\n rows: Array<Record<string, any>>,\n hint: PointBind | null,\n): PointRoleResolution {\n const backfilled: string[] = [];\n const refused: string[] = [];\n const out: PointBind = {};\n const colSet = new Set(columns);\n const valuesOf = (name: string) => rows.map(r => r[name]);\n\n // ---- 1. Carry over the hint, VERIFYING the two roles that can be catastrophically\n // wrong. city/lat/lon are carried as given: a mis-hinted city degrades to \"no match\"\n // (a visible off-map count), whereas a mis-hinted STATE actively relocates points.\n for (const role of [\"city\", \"zip\", \"lat\", \"lon\", \"country\"] as const) {\n const name = hint?.[role];\n if (name && colSet.has(name)) out[role] = name;\n }\n const hintedState = hint?.state;\n if (hintedState && colSet.has(hintedState)) {\n const vals = valuesOf(hintedState);\n if (looksLikeCountryColumn(vals)) {\n // The \"CA\" trap. Every value is a country, so this is a country column that\n // landed in the state slot. Using it would resolve \"CA\" to California and\n // drag a whole country's cities into one US state.\n //\n // Having identified what the column actually IS, put it to work rather than\n // discarding it: as the COUNTRY role it narrows city matching to the right\n // country, which is most of the value the state would have provided.\n refused.push(`state=${hintedState} (every value is a country, not a state)`);\n if (!out.country) out.country = hintedState;\n } else if (admin1MatchPct(vals) < THRESHOLD_PCT) {\n refused.push(`state=${hintedState} (only ${admin1MatchPct(vals)}% of values are states/provinces)`);\n } else {\n out.state = hintedState;\n }\n }\n\n // ---- 2. Backfill the roles nothing supplied. This is the real-world fix: the model\n // named only the city, so the state column sat unused beside it.\n const taken = new Set(Object.values(out).filter(Boolean) as string[]);\n const candidates = columns.filter(c => !taken.has(c) && !c.startsWith(\"__\"));\n\n if (!out.state) {\n let best: { name: string; pct: number } | null = null;\n for (const c of candidates) {\n const vals = valuesOf(c);\n if (looksLikeCountryColumn(vals)) continue; // never a state\n const pct = admin1MatchPct(vals);\n if (pct < THRESHOLD_PCT) continue;\n if (distinctMatches(vals, v => !!resolveAdmin1(v)) < MIN_DISTINCT_BACKFILL) continue;\n if (!best || pct > best.pct) best = { name: c, pct };\n }\n if (best) {\n out.state = best.name;\n taken.add(best.name);\n backfilled.push(`state=${best.name} (${best.pct}% states/provinces)`);\n }\n }\n\n if (!out.zip) {\n for (const c of candidates) {\n if (taken.has(c)) continue;\n const vals = valuesOf(c);\n const pct = zipMatchPct(vals);\n if (pct < ZIP_THRESHOLD_PCT) continue;\n if (distinctRawMatches(vals, v => zipPrefixCandidates(v).length > 0) < MIN_DISTINCT_BACKFILL) continue;\n out.zip = c;\n taken.add(c);\n backfilled.push(`zip=${c} (${pct}% ZIP codes)`);\n break;\n }\n }\n\n // COUNTRY narrows city matching to the row's own country, which is what keeps a bare\n // \"Burlington\" on a US row out of Ontario. Backfilled last among the narrowing roles\n // and only from an unambiguous all-country column, because a wrong country is worse\n // than none: it would filter every candidate away and drop the row to a coarser tier.\n if (!out.country) {\n for (const c of candidates) {\n if (taken.has(c)) continue;\n if (!looksLikeCountryColumn(valuesOf(c))) continue;\n out.country = c;\n taken.add(c);\n backfilled.push(`country=${c}`);\n break;\n }\n }\n\n if (!out.city) {\n let best: { name: string; pct: number } | null = null;\n for (const c of candidates) {\n if (taken.has(c)) continue;\n const vals = valuesOf(c);\n const pct = cityPct(vals);\n if (pct < THRESHOLD_PCT) continue;\n if (distinctMatches(vals, v => isKnownCity(v)) < MIN_DISTINCT_BACKFILL) continue;\n if (!best || pct > best.pct) best = { name: c, pct };\n }\n if (best) {\n out.city = best.name;\n backfilled.push(`city=${best.name} (${best.pct}% known cities)`);\n }\n }\n\n // country ALONE is not a placeable binding \u2014 it only narrows the other tiers \u2014 so it\n // deliberately does not count toward \"we can geocode something\".\n const any = !!(out.city || out.state || out.zip || (out.lat && out.lon));\n return { bind: any ? out : null, backfilled, refused };\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA,IAAM,gBAAgB;AAItB,IAAM,wBAAwB;AAO9B,IAAM,oBAAoB;AAG1B,IAAM,oBAAoB;AAK1B,IAAM,cAAc,oBAAI,IAAI;AAAA,EACxB;AAAA,EAAM;AAAA,EAAO;AAAA,EAAO;AAAA,EAAS;AAAA,EAAiB;AAAA,EAA4B;AAAA,EAC1E;AAAA,EAAM;AAAA,EAAO;AAAA,EACb;AAAA,EAAM;AAAA,EAAO;AAAA,EAAU;AAC3B,CAAC;AAGD,SAAS,mBAAmB,QAAkC;AAC1D,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,KAAK,QAAQ;AACpB,QAAI,MAAM,QAAQ,MAAM,OAAW;AACnC,UAAM,IAAI,mBAAmB,OAAO,CAAC,CAAC;AACtC,QAAI,CAAC,EAAG;AACR,QAAI,CAAC,KAAK,IAAI,CAAC,GAAG;AACd,WAAK,IAAI,CAAC;AACV,UAAI,KAAK,QAAQ,kBAAmB;AAAA,IACxC;AAAA,EACJ;AACA,SAAO,MAAM,KAAK,IAAI;AAC1B;
|
|
4
|
+
"sourcesContent": ["// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// POINT-COLUMN ROLE RESOLUTION \u2014 which column is the city, the state, the ZIP\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// geoPoint.ts answers \"given a city/state/zip, WHERE is this row?\". This module answers\n// the question that comes first: \"which COLUMN is the city, and which is the state?\".\n//\n// That used to be answered by the model: the codegen response names pointCityColumn /\n// pointStateColumn / pointZipColumn and the host geocoded from whatever it was handed.\n// A production North America point map showed why that is not sufficient. The model was\n// City{distinct=23}, StateCode{distinct=16}, Country{distinct=4}, <2 measures>\n// and answered `{\"city\":\"City\"}` \u2014 no state. Geocoding then ran city-only, so the\n// City+State tier never engaged, the \"state contradicts city\" guard never had a state\n// to check, and 8 of 32 marks were placed by the largest-match tie-break on a bare\n// name (\"Burlington\" -> Burlington ONTARIO, not Vermont) with the map presenting them\n// as ordinary points. Nothing was broken; a field was simply left blank.\n//\n// These roles are DETERMINISTICALLY RESOLVABLE from the values, and every classifier\n// needed already exists in geoPoint.ts. So the model's answer is treated as a HINT that\n// gets VERIFIED, and any role it omitted is BACKFILLED from the data. A hint is still\n// useful \u2014 it disambiguates which of two plausible columns was intended \u2014 but it is\n// never the only thing standing between the user and a wrong coordinate.\n//\n// THE COUNTRY TRAP this also closes. `resolveAdmin1(\"CA\")` is CALIFORNIA. Measured\n// across 15 country identifiers (\"US\" \"USA\" \"United States\" \"CAN\" \"Canada\" \"MX\" \"MEX\"\n// \"Mexico\" \u2026) that is the ONLY collision \u2014 but it is a catastrophic one: a Country\n// column bound into the state slot makes every Canadian city fail to match inside\n// California and fall back to the California centroid, piling Mississauga, Montr\u00E9al,\n// Burnaby and Laval onto one dot outside Bakersfield. \"Canada, inside the USA.\"\n//\n// It cannot be fixed the way the sibling \"Mexico\" collision was (geoPoint.ts drops the\n// bare admin1 NAME \"mexico\" because the country reading dominates and the State of\n// M\u00E9xico is obscure). For \"CA\" the California reading is overwhelmingly the common one\n// in real US data, so deleting the key would break far more than it fixes. The fix has\n// to be CONTEXTUAL, which is exactly what a column-level pass can do and a row-level\n// lookup cannot: judge the column by ALL its values at once.\n\nimport { normalizePlaceName, resolveAdmin1, isKnownCity, zipPrefixCandidates } from \"./geoPoint\";\n\n/** The place parts a coordinate can be resolved from. Any subset. */\nexport type PointBind = {\n city?: string;\n state?: string;\n zip?: string;\n lat?: string;\n lon?: string;\n /** Narrows a city name to the row's own country. Never inferred from a value in\n * another slot \u2014 see the \"CA\" note above. */\n country?: string;\n};\n\nexport type PointRoleResolution = {\n /** The binding to geocode with: hint, minus anything refused, plus anything backfilled. */\n bind: PointBind | null;\n /** Roles the hint omitted that were resolved from the data (\"state=StateCode\"). */\n backfilled: string[];\n /** Roles the hint named that were REFUSED, with why (\"state=Country (country column)\"). */\n refused: string[];\n};\n\n// Share of DISTINCT values a column must match before a role is claimed for it. Mirrors\n// geoDetector's THRESHOLD_PCT: high enough that a column of something else cannot claim\n// a role by accident, loose enough to tolerate the blanks and typos real data carries.\nconst THRESHOLD_PCT = 85;\n// A role BACKFILLED from the data (no hint) additionally needs this many distinct\n// matches \u2014 geoDetector's roster guard. One matching value is a coincidence, not a\n// column: a lone \"CA\" is far likelier to be Canada than a single-state California table.\nconst MIN_DISTINCT_BACKFILL = 2;\n// ZIP is held to a HIGHER bar than the name roles, because its classifier is pure digit\n// shape and measures are digits too: a Revenue column carrying 54300 / 48900 / 33700\n// reads as a perfectly good run of 5-digit ZIPs. Callers are expected to keep measures\n// out of the candidate list, but that is one `isMeasure` flag away from failing open, and\n// a measure silently adopted as the ZIP column would relocate every point on the map. A\n// real ZIP column is ALL ZIPs, so demand exactly that.\nconst ZIP_THRESHOLD_PCT = 100;\n// Classification reads DISTINCT values, so a wide column costs no more than a narrow\n// one; this only bounds a pathological all-unique text column.\nconst MAX_DISTINCT_SCAN = 400;\n\n// Country identifiers, normalized. Deliberately NOT a general ISO table: this exists\n// only to break the one measured admin1 collision, and a column of countries is\n// recognized by ALL its values being country identifiers, not by any single one.\nconst COUNTRY_IDS = new Set([\n \"us\", \"usa\", \"u s\", \"u s a\", \"united states\", \"united states of america\", \"america\",\n \"ca\", \"can\", \"canada\",\n \"mx\", \"mex\", \"mexico\", \"estados unidos mexicanos\",\n]);\n\n/** Distinct, normalized, non-blank values of a column (capped). */\nfunction distinctNormalized(values: Array<unknown>): string[] {\n const seen = new Set<string>();\n for (const v of values) {\n if (v === null || v === undefined) continue;\n const k = normalizePlaceName(String(v));\n if (!k) continue;\n if (!seen.has(k)) {\n seen.add(k);\n if (seen.size >= MAX_DISTINCT_SCAN) break;\n }\n }\n return Array.from(seen);\n}\n\n/**\n * Is this column a list of COUNTRIES?\n *\n * Every distinct value must be a country identifier, AND at least one of them must be\n * UNAMBIGUOUS \u2014 a country identifier that is not also a state/province code.\n *\n * That second condition is not fussiness, it is the whole correctness of this function\n * under CROSS-FILTERING. Roles are re-resolved against whatever rows survive a filter, and\n * \"CA\" is the one token that is both a country (Canada) and an admin1 (California). Filter\n * a US table down to a single Californian row and its StateCode column becomes exactly\n * {\"CA\"} \u2014 trivially \"all country identifiers\". Without this condition that column is\n * adopted as the COUNTRY role, every city is then narrowed to Canada, nothing matches, and\n * the map goes BLANK on a single click. Observed on the real point map, 2026-07-26.\n *\n * So: {US, CA, MX} is a country column (US and MX are unambiguous), {Canada, Mexico} is a\n * country column, {CA, TX} is a US state column containing California, and {CA} ALONE is\n * not enough evidence for anything \u2014 which is the honest answer, because it genuinely is\n * not enough evidence.\n */\nexport function looksLikeCountryColumn(values: Array<unknown>): boolean {\n const d = distinctNormalized(values);\n if (d.length === 0) return false;\n if (!d.every(v => COUNTRY_IDS.has(v))) return false;\n return d.some(v => !resolveAdmin1(v));\n}\n\n/** A token that is BOTH a country identifier and a state/province code \u2014 today only \"CA\"\n * (Canada vs California). Such a value can never, on its own, decide a column's role. */\nfunction isAmbiguousCountryState(normalized: string): boolean {\n return COUNTRY_IDS.has(normalized) && !!resolveAdmin1(normalized);\n}\n\n/** Share of DISTINCT values that resolve to a state/province, 0..100 (one decimal). */\nexport function admin1MatchPct(values: Array<unknown>): number {\n const d = distinctNormalized(values);\n if (d.length === 0) return 0;\n let n = 0;\n for (const v of d) if (resolveAdmin1(v)) n++;\n return Math.round((n / d.length) * 1000) / 10;\n}\n\n/** Distinct RAW (trimmed) values \u2014 for ZIPs, which normalizePlaceName would mangle:\n * it rewrites punctuation to spaces, turning \"90210-1234\" into \"90210 1234\", which\n * zipPrefixCandidates' ZIP+4 pattern then rejects. */\nfunction distinctRaw(values: Array<unknown>): string[] {\n const seen = new Set<string>();\n for (const v of values) {\n if (v === null || v === undefined) continue;\n const s = String(v).trim();\n if (!s) continue;\n if (!seen.has(s)) {\n seen.add(s);\n if (seen.size >= MAX_DISTINCT_SCAN) break;\n }\n }\n return Array.from(seen);\n}\n\n/** Share of DISTINCT values that read as a ZIP / ZIP-prefix, 0..100 (one decimal). */\nexport function zipMatchPct(values: Array<unknown>): number {\n const d = distinctRaw(values);\n if (d.length === 0) return 0;\n let n = 0;\n for (const v of d) if (zipPrefixCandidates(v).length > 0) n++;\n return Math.round((n / d.length) * 1000) / 10;\n}\n\n/** Share of DISTINCT values that are known city names, 0..100 (one decimal). */\nfunction cityPct(values: Array<unknown>): number {\n const d = distinctNormalized(values);\n if (d.length === 0) return 0;\n let n = 0;\n for (const v of d) if (isKnownCity(v)) n++;\n return Math.round((n / d.length) * 1000) / 10;\n}\n\nfunction distinctMatches(values: Array<unknown>, pred: (v: string) => boolean): number {\n let n = 0;\n for (const v of distinctNormalized(values)) if (pred(v)) n++;\n return n;\n}\n\n/** distinctMatches over RAW values (ZIP only \u2014 see distinctRaw). */\nfunction distinctRawMatches(values: Array<unknown>, pred: (v: string) => boolean): number {\n let n = 0;\n for (const v of distinctRaw(values)) if (pred(v)) n++;\n return n;\n}\n\n/**\n * Verify the hinted point-column roles against the data and fill in what the hint left\n * out. Pure: `columns` are the available column names, `rows` the row objects keyed by\n * those names, `hint` whatever the codegen response named (may be null).\n *\n * `columns` MUST exclude measures. A place is a dimension; a measure that wandered into\n * this list can be adopted as the ZIP column on digit shape alone (see ZIP_THRESHOLD_PCT)\n * and would move every point on the map.\n *\n * A role is only ever REFUSED for a positive reason (the column is countries; the column\n * does not look like states at all) \u2014 never merely because the classifier is unsure. And\n * a refusal drops that ONE role, leaving the rest of the binding intact, because\n * geocoding from city alone still beats geocoding from nothing.\n */\nexport function resolvePointRoles(\n columns: string[],\n rows: Array<Record<string, any>>,\n hint: PointBind | null,\n): PointRoleResolution {\n const backfilled: string[] = [];\n const refused: string[] = [];\n const out: PointBind = {};\n const colSet = new Set(columns);\n const valuesOf = (name: string) => rows.map(r => r[name]);\n\n // ---- 1. Carry over the hint, VERIFYING the two roles that can be catastrophically\n // wrong. city/lat/lon are carried as given: a mis-hinted city degrades to \"no match\"\n // (a visible off-map count), whereas a mis-hinted STATE actively relocates points.\n for (const role of [\"city\", \"zip\", \"lat\", \"lon\", \"country\"] as const) {\n const name = hint?.[role];\n if (name && colSet.has(name)) out[role] = name;\n }\n const hintedState = hint?.state;\n if (hintedState && colSet.has(hintedState)) {\n const vals = valuesOf(hintedState);\n const dvals = distinctNormalized(vals);\n if (dvals.length > 0 && dvals.every(isAmbiguousCountryState)) {\n // Everything in this column is the Canada/California token and nothing else.\n // It is genuinely undecidable, so decide nothing: refuse it as a state (using it\n // would place Canadian cities in California) and do NOT promote it to country\n // (that would place Californian cities in Canada). Falling back to city-only\n // still resolves, which is the one outcome that cannot be badly wrong.\n refused.push(`state=${hintedState} (every value is \"CA\", which is both Canada and California)`);\n } else if (looksLikeCountryColumn(vals)) {\n // The \"CA\" trap. Every value is a country, so this is a country column that\n // landed in the state slot. Using it would resolve \"CA\" to California and\n // drag a whole country's cities into one US state.\n //\n // Having identified what the column actually IS, put it to work rather than\n // discarding it: as the COUNTRY role it narrows city matching to the right\n // country, which is most of the value the state would have provided.\n refused.push(`state=${hintedState} (every value is a country, not a state)`);\n if (!out.country) out.country = hintedState;\n } else if (admin1MatchPct(vals) < THRESHOLD_PCT) {\n refused.push(`state=${hintedState} (only ${admin1MatchPct(vals)}% of values are states/provinces)`);\n } else {\n out.state = hintedState;\n }\n }\n\n // ---- 2. Backfill the roles nothing supplied. This is the real-world fix: the model\n // named only the city, so the state column sat unused beside it.\n const taken = new Set(Object.values(out).filter(Boolean) as string[]);\n const candidates = columns.filter(c => !taken.has(c) && !c.startsWith(\"__\"));\n\n if (!out.state) {\n let best: { name: string; pct: number } | null = null;\n for (const c of candidates) {\n const vals = valuesOf(c);\n if (looksLikeCountryColumn(vals)) continue; // never a state\n const pct = admin1MatchPct(vals);\n if (pct < THRESHOLD_PCT) continue;\n // The roster guard, relaxed for UNAMBIGUOUS evidence. A cross-filter can shrink\n // any column to one distinct value, and demanding two flatly would drop the\n // state role on every single-row subset \u2014 degrading precision for no reason. One\n // \"TX\" is proof enough; one \"CA\" is not, because it is equally Canada.\n const hits = distinctMatches(vals, v => !!resolveAdmin1(v));\n const unambiguous = distinctMatches(vals, v => !!resolveAdmin1(v) && !isAmbiguousCountryState(v));\n if (hits < MIN_DISTINCT_BACKFILL && unambiguous < 1) continue;\n if (!best || pct > best.pct) best = { name: c, pct };\n }\n if (best) {\n out.state = best.name;\n taken.add(best.name);\n backfilled.push(`state=${best.name} (${best.pct}% states/provinces)`);\n }\n }\n\n if (!out.zip) {\n for (const c of candidates) {\n if (taken.has(c)) continue;\n const vals = valuesOf(c);\n const pct = zipMatchPct(vals);\n if (pct < ZIP_THRESHOLD_PCT) continue;\n if (distinctRawMatches(vals, v => zipPrefixCandidates(v).length > 0) < MIN_DISTINCT_BACKFILL) continue;\n out.zip = c;\n taken.add(c);\n backfilled.push(`zip=${c} (${pct}% ZIP codes)`);\n break;\n }\n }\n\n // COUNTRY narrows city matching to the row's own country, which is what keeps a bare\n // \"Burlington\" on a US row out of Ontario. Backfilled last among the narrowing roles\n // and only from an unambiguous all-country column, because a wrong country is worse\n // than none: it would filter every candidate away and drop the row to a coarser tier.\n if (!out.country) {\n for (const c of candidates) {\n if (taken.has(c)) continue;\n if (!looksLikeCountryColumn(valuesOf(c))) continue;\n out.country = c;\n taken.add(c);\n backfilled.push(`country=${c}`);\n break;\n }\n }\n\n if (!out.city) {\n let best: { name: string; pct: number } | null = null;\n for (const c of candidates) {\n if (taken.has(c)) continue;\n const vals = valuesOf(c);\n const pct = cityPct(vals);\n if (pct < THRESHOLD_PCT) continue;\n if (distinctMatches(vals, v => isKnownCity(v)) < MIN_DISTINCT_BACKFILL) continue;\n if (!best || pct > best.pct) best = { name: c, pct };\n }\n if (best) {\n out.city = best.name;\n backfilled.push(`city=${best.name} (${best.pct}% known cities)`);\n }\n }\n\n // country ALONE is not a placeable binding \u2014 it only narrows the other tiers \u2014 so it\n // deliberately does not count toward \"we can geocode something\".\n const any = !!(out.city || out.state || out.zip || (out.lat && out.lon));\n return { bind: any ? out : null, backfilled, refused };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -20,10 +20,21 @@ export type PointRoleResolution = {
|
|
|
20
20
|
/**
|
|
21
21
|
* Is this column a list of COUNTRIES?
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Every distinct value must be a country identifier, AND at least one of them must be
|
|
24
|
+
* UNAMBIGUOUS — a country identifier that is not also a state/province code.
|
|
25
|
+
*
|
|
26
|
+
* That second condition is not fussiness, it is the whole correctness of this function
|
|
27
|
+
* under CROSS-FILTERING. Roles are re-resolved against whatever rows survive a filter, and
|
|
28
|
+
* "CA" is the one token that is both a country (Canada) and an admin1 (California). Filter
|
|
29
|
+
* a US table down to a single Californian row and its StateCode column becomes exactly
|
|
30
|
+
* {"CA"} — trivially "all country identifiers". Without this condition that column is
|
|
31
|
+
* adopted as the COUNTRY role, every city is then narrowed to Canada, nothing matches, and
|
|
32
|
+
* the map goes BLANK on a single click. Observed on the real point map, 2026-07-26.
|
|
33
|
+
*
|
|
34
|
+
* So: {US, CA, MX} is a country column (US and MX are unambiguous), {Canada, Mexico} is a
|
|
35
|
+
* country column, {CA, TX} is a US state column containing California, and {CA} ALONE is
|
|
36
|
+
* not enough evidence for anything — which is the honest answer, because it genuinely is
|
|
37
|
+
* not enough evidence.
|
|
27
38
|
*/
|
|
28
39
|
export declare function looksLikeCountryColumn(values: Array<unknown>): boolean;
|
|
29
40
|
/** Share of DISTINCT values that resolve to a state/province, 0..100 (one decimal). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bicharts/shape-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Host-agnostic data-shape profiler: ingest a dataset's rows through IValueCollection.addRow and read back the measured shape as an LLMColumnWithValue[] payload — types, cardinality, temporal/ordinal detection, geographic region and point resolution, cross-column signals. MEASUREMENT only: the policy that decides what a measured shape means for chart selection lives server-side.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|