@bicharts/shape-core 0.1.5 → 0.1.6
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 +159 -0
- package/dist/index.mjs.map +3 -3
- package/dist/types/geoPointRoles.d.ts +44 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import {
|
|
22
22
|
buildGeoPointColumns,
|
|
23
23
|
cityMatchPct,
|
|
24
|
+
isKnownCity,
|
|
24
25
|
normalizePlaceName,
|
|
25
26
|
resolveAdmin1,
|
|
26
27
|
resolveGeoPoint,
|
|
@@ -39,11 +40,166 @@ import {
|
|
|
39
40
|
monthLookupFor,
|
|
40
41
|
normalizeMonthKey
|
|
41
42
|
} from "./chunk-RZAZ7PZK.mjs";
|
|
43
|
+
|
|
44
|
+
// src/geoPointRoles.ts
|
|
45
|
+
var THRESHOLD_PCT = 85;
|
|
46
|
+
var MIN_DISTINCT_BACKFILL = 2;
|
|
47
|
+
var ZIP_THRESHOLD_PCT = 100;
|
|
48
|
+
var MAX_DISTINCT_SCAN = 400;
|
|
49
|
+
var COUNTRY_IDS = /* @__PURE__ */ new Set([
|
|
50
|
+
"us",
|
|
51
|
+
"usa",
|
|
52
|
+
"u s",
|
|
53
|
+
"u s a",
|
|
54
|
+
"united states",
|
|
55
|
+
"united states of america",
|
|
56
|
+
"america",
|
|
57
|
+
"ca",
|
|
58
|
+
"can",
|
|
59
|
+
"canada",
|
|
60
|
+
"mx",
|
|
61
|
+
"mex",
|
|
62
|
+
"mexico",
|
|
63
|
+
"estados unidos mexicanos"
|
|
64
|
+
]);
|
|
65
|
+
function distinctNormalized(values) {
|
|
66
|
+
const seen = /* @__PURE__ */ new Set();
|
|
67
|
+
for (const v of values) {
|
|
68
|
+
if (v === null || v === void 0) continue;
|
|
69
|
+
const k = normalizePlaceName(String(v));
|
|
70
|
+
if (!k) continue;
|
|
71
|
+
if (!seen.has(k)) {
|
|
72
|
+
seen.add(k);
|
|
73
|
+
if (seen.size >= MAX_DISTINCT_SCAN) break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return Array.from(seen);
|
|
77
|
+
}
|
|
78
|
+
function looksLikeCountryColumn(values) {
|
|
79
|
+
const d = distinctNormalized(values);
|
|
80
|
+
if (d.length === 0) return false;
|
|
81
|
+
return d.every((v) => COUNTRY_IDS.has(v));
|
|
82
|
+
}
|
|
83
|
+
function admin1MatchPct(values) {
|
|
84
|
+
const d = distinctNormalized(values);
|
|
85
|
+
if (d.length === 0) return 0;
|
|
86
|
+
let n = 0;
|
|
87
|
+
for (const v of d) if (resolveAdmin1(v)) n++;
|
|
88
|
+
return Math.round(n / d.length * 1e3) / 10;
|
|
89
|
+
}
|
|
90
|
+
function distinctRaw(values) {
|
|
91
|
+
const seen = /* @__PURE__ */ new Set();
|
|
92
|
+
for (const v of values) {
|
|
93
|
+
if (v === null || v === void 0) continue;
|
|
94
|
+
const s = String(v).trim();
|
|
95
|
+
if (!s) continue;
|
|
96
|
+
if (!seen.has(s)) {
|
|
97
|
+
seen.add(s);
|
|
98
|
+
if (seen.size >= MAX_DISTINCT_SCAN) break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return Array.from(seen);
|
|
102
|
+
}
|
|
103
|
+
function zipMatchPct(values) {
|
|
104
|
+
const d = distinctRaw(values);
|
|
105
|
+
if (d.length === 0) return 0;
|
|
106
|
+
let n = 0;
|
|
107
|
+
for (const v of d) if (zipPrefixCandidates(v).length > 0) n++;
|
|
108
|
+
return Math.round(n / d.length * 1e3) / 10;
|
|
109
|
+
}
|
|
110
|
+
function cityPct(values) {
|
|
111
|
+
const d = distinctNormalized(values);
|
|
112
|
+
if (d.length === 0) return 0;
|
|
113
|
+
let n = 0;
|
|
114
|
+
for (const v of d) if (isKnownCity(v)) n++;
|
|
115
|
+
return Math.round(n / d.length * 1e3) / 10;
|
|
116
|
+
}
|
|
117
|
+
function distinctMatches(values, pred) {
|
|
118
|
+
let n = 0;
|
|
119
|
+
for (const v of distinctNormalized(values)) if (pred(v)) n++;
|
|
120
|
+
return n;
|
|
121
|
+
}
|
|
122
|
+
function distinctRawMatches(values, pred) {
|
|
123
|
+
let n = 0;
|
|
124
|
+
for (const v of distinctRaw(values)) if (pred(v)) n++;
|
|
125
|
+
return n;
|
|
126
|
+
}
|
|
127
|
+
function resolvePointRoles(columns, rows, hint) {
|
|
128
|
+
const backfilled = [];
|
|
129
|
+
const refused = [];
|
|
130
|
+
const out = {};
|
|
131
|
+
const colSet = new Set(columns);
|
|
132
|
+
const valuesOf = (name) => rows.map((r) => r[name]);
|
|
133
|
+
for (const role of ["city", "zip", "lat", "lon"]) {
|
|
134
|
+
const name = hint?.[role];
|
|
135
|
+
if (name && colSet.has(name)) out[role] = name;
|
|
136
|
+
}
|
|
137
|
+
const hintedState = hint?.state;
|
|
138
|
+
if (hintedState && colSet.has(hintedState)) {
|
|
139
|
+
const vals = valuesOf(hintedState);
|
|
140
|
+
if (looksLikeCountryColumn(vals)) {
|
|
141
|
+
refused.push(`state=${hintedState} (every value is a country, not a state)`);
|
|
142
|
+
} else if (admin1MatchPct(vals) < THRESHOLD_PCT) {
|
|
143
|
+
refused.push(`state=${hintedState} (only ${admin1MatchPct(vals)}% of values are states/provinces)`);
|
|
144
|
+
} else {
|
|
145
|
+
out.state = hintedState;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const taken = new Set(Object.values(out).filter(Boolean));
|
|
149
|
+
const candidates = columns.filter((c) => !taken.has(c) && !c.startsWith("__"));
|
|
150
|
+
if (!out.state) {
|
|
151
|
+
let best = null;
|
|
152
|
+
for (const c of candidates) {
|
|
153
|
+
const vals = valuesOf(c);
|
|
154
|
+
if (looksLikeCountryColumn(vals)) continue;
|
|
155
|
+
const pct = admin1MatchPct(vals);
|
|
156
|
+
if (pct < THRESHOLD_PCT) continue;
|
|
157
|
+
if (distinctMatches(vals, (v) => !!resolveAdmin1(v)) < MIN_DISTINCT_BACKFILL) continue;
|
|
158
|
+
if (!best || pct > best.pct) best = { name: c, pct };
|
|
159
|
+
}
|
|
160
|
+
if (best) {
|
|
161
|
+
out.state = best.name;
|
|
162
|
+
taken.add(best.name);
|
|
163
|
+
backfilled.push(`state=${best.name} (${best.pct}% states/provinces)`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!out.zip) {
|
|
167
|
+
for (const c of candidates) {
|
|
168
|
+
if (taken.has(c)) continue;
|
|
169
|
+
const vals = valuesOf(c);
|
|
170
|
+
const pct = zipMatchPct(vals);
|
|
171
|
+
if (pct < ZIP_THRESHOLD_PCT) continue;
|
|
172
|
+
if (distinctRawMatches(vals, (v) => zipPrefixCandidates(v).length > 0) < MIN_DISTINCT_BACKFILL) continue;
|
|
173
|
+
out.zip = c;
|
|
174
|
+
taken.add(c);
|
|
175
|
+
backfilled.push(`zip=${c} (${pct}% ZIP codes)`);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!out.city) {
|
|
180
|
+
let best = null;
|
|
181
|
+
for (const c of candidates) {
|
|
182
|
+
if (taken.has(c)) continue;
|
|
183
|
+
const vals = valuesOf(c);
|
|
184
|
+
const pct = cityPct(vals);
|
|
185
|
+
if (pct < THRESHOLD_PCT) continue;
|
|
186
|
+
if (distinctMatches(vals, (v) => isKnownCity(v)) < MIN_DISTINCT_BACKFILL) continue;
|
|
187
|
+
if (!best || pct > best.pct) best = { name: c, pct };
|
|
188
|
+
}
|
|
189
|
+
if (best) {
|
|
190
|
+
out.city = best.name;
|
|
191
|
+
backfilled.push(`city=${best.name} (${best.pct}% known cities)`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const any = !!(out.city || out.state || out.zip || out.lat && out.lon);
|
|
195
|
+
return { bind: any ? out : null, backfilled, refused };
|
|
196
|
+
}
|
|
42
197
|
export {
|
|
43
198
|
GET_RANDOM,
|
|
44
199
|
IndexedText,
|
|
45
200
|
SIMPLE_STRING_HASH,
|
|
46
201
|
STR,
|
|
202
|
+
admin1MatchPct,
|
|
47
203
|
buildGeoIsoColumn,
|
|
48
204
|
buildGeoPointColumns,
|
|
49
205
|
cityMatchPct,
|
|
@@ -57,13 +213,16 @@ export {
|
|
|
57
213
|
isIdentifierName,
|
|
58
214
|
isJoinGeoKind,
|
|
59
215
|
isOrdinalFriendlyName,
|
|
216
|
+
looksLikeCountryColumn,
|
|
60
217
|
monthLookupFor,
|
|
61
218
|
normalizeMonthKey,
|
|
62
219
|
normalizePlaceName,
|
|
63
220
|
resolveAdmin1,
|
|
64
221
|
resolveGeoPoint,
|
|
222
|
+
resolvePointRoles,
|
|
65
223
|
safeDistinctValuesToShip,
|
|
66
224
|
toGeoIso,
|
|
225
|
+
zipMatchPct,
|
|
67
226
|
zipPrefixCandidates,
|
|
68
227
|
zipToPrefix3
|
|
69
228
|
};
|
package/dist/index.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": [],
|
|
4
|
-
"sourcesContent": [],
|
|
5
|
-
"mappings": "",
|
|
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};\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\"] 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 refused.push(`state=${hintedState} (every value is a country, not a state)`);\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 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 const any = !!(out.city || out.state || out.zip || (out.lat && out.lon));\n return { bind: any ? out : null, backfilled, refused };\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,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;AAUO,SAAS,uBAAuB,QAAiC;AACpE,QAAM,IAAI,mBAAmB,MAAM;AACnC,MAAI,EAAE,WAAW,EAAG,QAAO;AAC3B,SAAO,EAAE,MAAM,OAAK,YAAY,IAAI,CAAC,CAAC;AAC1C;AAGO,SAAS,eAAe,QAAgC;AAC3D,QAAM,IAAI,mBAAmB,MAAM;AACnC,MAAI,EAAE,WAAW,EAAG,QAAO;AAC3B,MAAI,IAAI;AACR,aAAW,KAAK,EAAG,KAAI,cAAc,CAAC,EAAG;AACzC,SAAO,KAAK,MAAO,IAAI,EAAE,SAAU,GAAI,IAAI;AAC/C;AAKA,SAAS,YAAY,QAAkC;AACnD,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,KAAK,QAAQ;AACpB,QAAI,MAAM,QAAQ,MAAM,OAAW;AACnC,UAAM,IAAI,OAAO,CAAC,EAAE,KAAK;AACzB,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;AAGO,SAAS,YAAY,QAAgC;AACxD,QAAM,IAAI,YAAY,MAAM;AAC5B,MAAI,EAAE,WAAW,EAAG,QAAO;AAC3B,MAAI,IAAI;AACR,aAAW,KAAK,EAAG,KAAI,oBAAoB,CAAC,EAAE,SAAS,EAAG;AAC1D,SAAO,KAAK,MAAO,IAAI,EAAE,SAAU,GAAI,IAAI;AAC/C;AAGA,SAAS,QAAQ,QAAgC;AAC7C,QAAM,IAAI,mBAAmB,MAAM;AACnC,MAAI,EAAE,WAAW,EAAG,QAAO;AAC3B,MAAI,IAAI;AACR,aAAW,KAAK,EAAG,KAAI,YAAY,CAAC,EAAG;AACvC,SAAO,KAAK,MAAO,IAAI,EAAE,SAAU,GAAI,IAAI;AAC/C;AAEA,SAAS,gBAAgB,QAAwB,MAAsC;AACnF,MAAI,IAAI;AACR,aAAW,KAAK,mBAAmB,MAAM,EAAG,KAAI,KAAK,CAAC,EAAG;AACzD,SAAO;AACX;AAGA,SAAS,mBAAmB,QAAwB,MAAsC;AACtF,MAAI,IAAI;AACR,aAAW,KAAK,YAAY,MAAM,EAAG,KAAI,KAAK,CAAC,EAAG;AAClD,SAAO;AACX;AAgBO,SAAS,kBACZ,SACA,MACA,MACmB;AACnB,QAAM,aAAuB,CAAC;AAC9B,QAAM,UAAoB,CAAC;AAC3B,QAAM,MAAiB,CAAC;AACxB,QAAM,SAAS,IAAI,IAAI,OAAO;AAC9B,QAAM,WAAW,CAAC,SAAiB,KAAK,IAAI,OAAK,EAAE,IAAI,CAAC;AAKxD,aAAW,QAAQ,CAAC,QAAQ,OAAO,OAAO,KAAK,GAAY;AACvD,UAAM,OAAO,OAAO,IAAI;AACxB,QAAI,QAAQ,OAAO,IAAI,IAAI,EAAG,KAAI,IAAI,IAAI;AAAA,EAC9C;AACA,QAAM,cAAc,MAAM;AAC1B,MAAI,eAAe,OAAO,IAAI,WAAW,GAAG;AACxC,UAAM,OAAO,SAAS,WAAW;AACjC,QAAI,uBAAuB,IAAI,GAAG;AAI9B,cAAQ,KAAK,SAAS,WAAW,0CAA0C;AAAA,IAC/E,WAAW,eAAe,IAAI,IAAI,eAAe;AAC7C,cAAQ,KAAK,SAAS,WAAW,UAAU,eAAe,IAAI,CAAC,mCAAmC;AAAA,IACtG,OAAO;AACH,UAAI,QAAQ;AAAA,IAChB;AAAA,EACJ;AAIA,QAAM,QAAQ,IAAI,IAAI,OAAO,OAAO,GAAG,EAAE,OAAO,OAAO,CAAa;AACpE,QAAM,aAAa,QAAQ,OAAO,OAAK,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,EAAE,WAAW,IAAI,CAAC;AAE3E,MAAI,CAAC,IAAI,OAAO;AACZ,QAAI,OAA6C;AACjD,eAAW,KAAK,YAAY;AACxB,YAAM,OAAO,SAAS,CAAC;AACvB,UAAI,uBAAuB,IAAI,EAAG;AAClC,YAAM,MAAM,eAAe,IAAI;AAC/B,UAAI,MAAM,cAAe;AACzB,UAAI,gBAAgB,MAAM,OAAK,CAAC,CAAC,cAAc,CAAC,CAAC,IAAI,sBAAuB;AAC5E,UAAI,CAAC,QAAQ,MAAM,KAAK,IAAK,QAAO,EAAE,MAAM,GAAG,IAAI;AAAA,IACvD;AACA,QAAI,MAAM;AACN,UAAI,QAAQ,KAAK;AACjB,YAAM,IAAI,KAAK,IAAI;AACnB,iBAAW,KAAK,SAAS,KAAK,IAAI,KAAK,KAAK,GAAG,qBAAqB;AAAA,IACxE;AAAA,EACJ;AAEA,MAAI,CAAC,IAAI,KAAK;AACV,eAAW,KAAK,YAAY;AACxB,UAAI,MAAM,IAAI,CAAC,EAAG;AAClB,YAAM,OAAO,SAAS,CAAC;AACvB,YAAM,MAAM,YAAY,IAAI;AAC5B,UAAI,MAAM,kBAAmB;AAC7B,UAAI,mBAAmB,MAAM,OAAK,oBAAoB,CAAC,EAAE,SAAS,CAAC,IAAI,sBAAuB;AAC9F,UAAI,MAAM;AACV,YAAM,IAAI,CAAC;AACX,iBAAW,KAAK,OAAO,CAAC,KAAK,GAAG,cAAc;AAC9C;AAAA,IACJ;AAAA,EACJ;AAEA,MAAI,CAAC,IAAI,MAAM;AACX,QAAI,OAA6C;AACjD,eAAW,KAAK,YAAY;AACxB,UAAI,MAAM,IAAI,CAAC,EAAG;AAClB,YAAM,OAAO,SAAS,CAAC;AACvB,YAAM,MAAM,QAAQ,IAAI;AACxB,UAAI,MAAM,cAAe;AACzB,UAAI,gBAAgB,MAAM,OAAK,YAAY,CAAC,CAAC,IAAI,sBAAuB;AACxE,UAAI,CAAC,QAAQ,MAAM,KAAK,IAAK,QAAO,EAAE,MAAM,GAAG,IAAI;AAAA,IACvD;AACA,QAAI,MAAM;AACN,UAAI,OAAO,KAAK;AAChB,iBAAW,KAAK,QAAQ,KAAK,IAAI,KAAK,KAAK,GAAG,iBAAiB;AAAA,IACnE;AAAA,EACJ;AAEA,QAAM,MAAM,CAAC,EAAE,IAAI,QAAQ,IAAI,SAAS,IAAI,OAAQ,IAAI,OAAO,IAAI;AACnE,SAAO,EAAE,MAAM,MAAM,MAAM,MAAM,YAAY,QAAQ;AACzD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** The place parts a coordinate can be resolved from. Any subset. */
|
|
2
|
+
export type PointBind = {
|
|
3
|
+
city?: string;
|
|
4
|
+
state?: string;
|
|
5
|
+
zip?: string;
|
|
6
|
+
lat?: string;
|
|
7
|
+
lon?: string;
|
|
8
|
+
};
|
|
9
|
+
export type PointRoleResolution = {
|
|
10
|
+
/** The binding to geocode with: hint, minus anything refused, plus anything backfilled. */
|
|
11
|
+
bind: PointBind | null;
|
|
12
|
+
/** Roles the hint omitted that were resolved from the data ("state=StateCode"). */
|
|
13
|
+
backfilled: string[];
|
|
14
|
+
/** Roles the hint named that were REFUSED, with why ("state=Country (country column)"). */
|
|
15
|
+
refused: string[];
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Is this column a list of COUNTRIES?
|
|
19
|
+
*
|
|
20
|
+
* True only when EVERY distinct value is a country identifier. That is the precise
|
|
21
|
+
* shape of the trap: {US, CA, MX} and {CA} are countries, while {CA, TX} is a US state
|
|
22
|
+
* column that happens to contain California. Being strict here matters — a loose test
|
|
23
|
+
* would start refusing legitimate state columns.
|
|
24
|
+
*/
|
|
25
|
+
export declare function looksLikeCountryColumn(values: Array<unknown>): boolean;
|
|
26
|
+
/** Share of DISTINCT values that resolve to a state/province, 0..100 (one decimal). */
|
|
27
|
+
export declare function admin1MatchPct(values: Array<unknown>): number;
|
|
28
|
+
/** Share of DISTINCT values that read as a ZIP / ZIP-prefix, 0..100 (one decimal). */
|
|
29
|
+
export declare function zipMatchPct(values: Array<unknown>): number;
|
|
30
|
+
/**
|
|
31
|
+
* Verify the hinted point-column roles against the data and fill in what the hint left
|
|
32
|
+
* out. Pure: `columns` are the available column names, `rows` the row objects keyed by
|
|
33
|
+
* those names, `hint` whatever the codegen response named (may be null).
|
|
34
|
+
*
|
|
35
|
+
* `columns` MUST exclude measures. A place is a dimension; a measure that wandered into
|
|
36
|
+
* this list can be adopted as the ZIP column on digit shape alone (see ZIP_THRESHOLD_PCT)
|
|
37
|
+
* and would move every point on the map.
|
|
38
|
+
*
|
|
39
|
+
* A role is only ever REFUSED for a positive reason (the column is countries; the column
|
|
40
|
+
* does not look like states at all) — never merely because the classifier is unsure. And
|
|
41
|
+
* a refusal drops that ONE role, leaving the rest of the binding intact, because
|
|
42
|
+
* geocoding from city alone still beats geocoding from nothing.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolvePointRoles(columns: string[], rows: Array<Record<string, any>>, hint: PointBind | null): PointRoleResolution;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -9,5 +9,7 @@ export { detectGeo, toGeoIso, buildGeoIsoColumn, isJoinGeoKind } from "./geoDete
|
|
|
9
9
|
export type { GeoKind, GeoDetectionResult, GeoIsoColumn } from "./geoDetector";
|
|
10
10
|
export { resolveGeoPoint, buildGeoPointColumns, resolveAdmin1, zipToPrefix3, zipPrefixCandidates, normalizePlaceName, cityMatchPct, } from "./geoPoint";
|
|
11
11
|
export type { GeoPointPrecision, GeoPointResult, GeoPointColumns } from "./geoPoint";
|
|
12
|
+
export { resolvePointRoles, looksLikeCountryColumn, admin1MatchPct, zipMatchPct, } from "./geoPointRoles";
|
|
13
|
+
export type { PointBind, PointRoleResolution } from "./geoPointRoles";
|
|
12
14
|
export { monthLookupFor, normalizeMonthKey } from "./monthNames";
|
|
13
15
|
export { STR, SIMPLE_STRING_HASH, GET_RANDOM } from "./util";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bicharts/shape-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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",
|