UPG Neighbor: the ring is a column on every row, with rop3 so other plugins can join their groups to it

This commit is contained in:
Doxa Maps 2026-09-28 13:52:21 +00:00
parent d679430a5c
commit 266ddef381
3 changed files with 13 additions and 12 deletions

File diff suppressed because one or more lines are too long

View file

@ -90,7 +90,7 @@ const groups = src.map((r, i) => {
if (!Number.isFinite(lat) || !Number.isFinite(lng)) fail(`no coordinates for ${r.PGID}`)
const pop = Number(r.Population) || 0
return {
i, pgid: r.PGID, name: r.DisplayName || r.Name, iso3: r.ISOAlpha3, country: r.CountryDisplayName || r.CountryName,
i, pgid: r.PGID, rop3: r.ROP3Code, name: r.DisplayName || r.Name, iso3: r.ISOAlpha3, country: r.CountryDisplayName || r.CountryName,
lat, lng, pop, reached: band[0], evPct: band[1], believers: band[0] ? Math.round(pop * band[1] / 100) : 0,
lang: (r.LanguageCode || '').trim(), langName: r.LanguageName || '', family: (r.LanguageFamily || '').trim(),
ethne: r.ROP25Code, cluster: r.ROP2Code, clusterName: r.ROP2Name, bloc: r.ROP1Code, blocName: r.ROP1Name,
@ -189,7 +189,10 @@ const ringCount = ringCounts(unreached, (u) => ringOfU.get(u.i)?.[0])
const shipped = groups.filter((g) => !g.reached || used.has(g.i))
const newIdx = new Map(shipped.map((g, j) => [g.i, j]))
const COLS = ['pgid', 'name', 'iso3', 'country', 'lat', 'lng', 'pop', 'reached', 'evPct', 'believers', 'lang', 'langName',
'family', 'ethne', 'cluster', 'clusterName', 'bloc', 'blocName', 'religion', 'engaged', 'bible', 'film', 'audio', 'radio']
'family', 'ethne', 'cluster', 'clusterName', 'bloc', 'blocName', 'religion', 'engaged', 'bible', 'film', 'audio', 'radio',
// rop3 + iso3 is the join key other plugins hold (DX groups: rop3 + country). ring = centered-set ring 1-10
// (0 = reached or no relation); ringNb = the shipped row of the reached group that sets it (-1 = none).
'rop3', 'ring', 'ringNb']
// Repeated strings are dictionary-encoded (the value in the row is an index into dict[col]); this
// halves the file. A reader decodes with dict[col][row[j]].
const DICT_COLS = ['iso3', 'country', 'lang', 'langName', 'family', 'cluster', 'clusterName', 'bloc', 'blocName', 'religion']
@ -197,6 +200,9 @@ const dict = Object.fromEntries(DICT_COLS.map((c) => [c, []]))
const dictIdx = Object.fromEntries(DICT_COLS.map((c) => [c, new Map()]))
const enc = (c, v) => { const m = dictIdx[c]; if (!m.has(v)) { m.set(v, dict[c].length); dict[c].push(v) } return m.get(v) }
const rows = shipped.map((g) => COLS.map((c) => {
const rg = ringOfU.get(g.i)
if (c === 'ring') return rg ? rg[0] : 0
if (c === 'ringNb') return rg ? newIdx.get(rg[1]) : -1
const v = g[c]
if (DICT_COLS.includes(c)) return enc(c, v)
if (typeof v === 'boolean') return v ? 1 : 0
@ -206,9 +212,6 @@ const rows = shipped.map((g) => COLS.map((c) => {
const outLinks = {}
for (const [ui, ls] of Object.entries(links)) outLinks[newIdx.get(+ui)] = ls.map(([ri, e, d, same]) => [newIdx.get(ri), e, d, same])
const outRings = {}
for (const [ui, [ring, ri]] of ringOfU) outRings[newIdx.get(ui)] = [ring, newIdx.get(ri)]
const outBridges = bridges.map((b) => ({
lang: b.lang, believers: b.believers, unreachedPop: b.unreachedPop, res: b.res,
reached: b.rs.map((g) => newIdx.get(g.i)), unreached: b.us.map((g) => newIdx.get(g.i)),
@ -224,7 +227,7 @@ const out = {
e2: 'different language, same ROP2 people cluster, or same LanguageFamily within the same ROP1 affinity bloc',
e3: `none of the above, within ${NEAR_KM} km or in the same country`,
p: 'P-level of an unreached group = the smallest E-level any reached neighbor reaches it at',
rings: 'rings[u] = [ring, reachedRow]. 1-3 same LanguageCode, 4-6 same LanguageName root (text before the comma), 7-9 same LanguageFamily, each x culture same ROP25 / near ROP2 / different; 10 = same country or within 600 km. Closest = lowest ring, then believers / (1 + (km/500)^2). Rule: src/rings.js',
rings: 'columns ring + ringNb on every row (ring 0 = reached or none; ringNb -1 = none). 1-3 same LanguageCode, 4-6 same LanguageName root (text before the comma), 7-9 same LanguageFamily, each x culture same ROP25 / near ROP2 / different; 10 = same country or within 600 km. Closest = lowest ring, then believers / (1 + (km/500)^2). Rule: src/rings.js',
kept: KEEP, rank: `inside one E-level: stronger match kind first (E-2: people cluster before language family), then estimated believers / (1 + (km/${HALF_KM})^2)`,
},
believersEst: 'Population x midpoint of the evangelical band: an order of magnitude for sizing an audience, not a count',
@ -242,7 +245,6 @@ const out = {
rows,
links: outLinks,
bridges: outBridges,
rings: outRings,
}
writeFileSync(OUT, JSON.stringify(out) + '\n')
const bytes = Buffer.byteLength(JSON.stringify(out))

View file

@ -27,7 +27,7 @@ export function loadNearData(profileConfig) {
}
function decode(raw) {
const { cols, dict, rows, links, bridges, meta, rings = {} } = raw
const { cols, dict, rows, links, bridges, meta } = raw
const groups = rows.map((r, i) => {
const g = { i }
cols.forEach((c, j) => { g[c] = dict[c] ? dict[c][r[j]] : r[j] })
@ -39,9 +39,8 @@ function decode(raw) {
const unreached = groups.filter((g) => !g.reached)
for (const u of unreached) {
u.p = pLevel(u)
const rg = rings[u.i] // centered-set ring (src/rings.js): 1-10, 0 = none; ringNeighbor = the reached group that sets it
u.ring = rg ? rg[0] : 0
u.ringNeighbor = rg ? groups[rg[1]] : null
// centered-set ring (src/rings.js): u.ring 1-10 arrives as a column (0 = none); ringNeighbor = the reached group that sets it
u.ringNeighbor = u.ringNb >= 0 ? groups[u.ringNb] : null
}
const bridgeList = bridges.map((b) => {
const rs = b.reached.map((i) => groups[i]), us = b.unreached.map((i) => groups[i])