Gospel x Internet reads each group's province from gospel-plugin
gospel-by-province-groups.json replaces this builder's own point-in-polygon (which agreed with it group for group: 0 province changes on rebuild). One assertion stays: the aggregate's groups sum to the per-group rows, so a stale copy of one file beside a fresh other refuses to build. The rebuild also inherits gospel-plugin's fix for 3 retired film links.
This commit is contained in:
parent
83c25a3204
commit
61e1d417f6
3 changed files with 29 additions and 50 deletions
File diff suppressed because one or more lines are too long
|
|
@ -10,19 +10,19 @@
|
|||
* data/tables/country-connectivity.json (World Bank) internet %, year, mobile per 100
|
||||
* data/tables/internet-by-country.json (cities-internet) top platform
|
||||
* data/tables/internet-by-adm1.json (exp-shepherd) India by state, NFHS-5
|
||||
* data/tables/gospel-by-province.json (gospel-plugin) the province aggregates this build must AGREE with
|
||||
* data/tables/gospel-by-province-groups.json (gospel-plugin) each IND/CHN group's province (approximate)
|
||||
* data/tables/gospel-by-province.json (gospel-plugin) the aggregates that per-group file must sum to
|
||||
*
|
||||
* ★ IT FAILS, IT NEVER DROPS. Every join is counted, and the build exits non-zero if the output has
|
||||
* fewer rows than the access table, if a key is duplicated, or if a group's country has no
|
||||
* connectivity row. A board that quietly lost 40 groups would say "nothing here" about real people.
|
||||
*
|
||||
* ★ THE PROVINCE IS COMPUTED HERE AND CHECKED THERE. gospel-by-province.json publishes per-province
|
||||
* aggregates, not per-group assignments, so this build places each IND/CHN group by point-in-polygon
|
||||
* on the SAME boundary files, and then REFUSES to write unless its per-province counts equal
|
||||
* gospel-by-province's `groups` for every province, and its unassigned count equals theirs. Two
|
||||
* builds that disagree about which state a group stands in would be two maps telling a mobiliser
|
||||
* different things; this makes that impossible to ship. (Asked gospel-plugin to publish the
|
||||
* per-group column so this copy can go.)
|
||||
* ★ THE PROVINCE IS READ, NEVER RE-DERIVED. gospel-plugin publishes each placed group's province
|
||||
* (gospel-by-province-groups.json, reconciled with its aggregates before either is written). This build
|
||||
* keeps ONE cheap assertion, the one gospel-plugin recommended: sum(aggregate groups) == per-group rows,
|
||||
* which catches a stale copy of one file beside a fresh copy of the other. Every IND/CHN group must be
|
||||
* either placed there or listed in the aggregate's `unassigned`, so none is silently province-less.
|
||||
* (It replaces this builder's own point-in-polygon, which agreed with theirs group for group.)
|
||||
*
|
||||
* ★ WHAT IS NEVER READ: doxa-people-groups.rows.engagement, or anything that says who works where
|
||||
* (SPEC-GOSPEL-INTERNET-DASHBOARD section 13). Nothing from plugin-designs/key-cities/.
|
||||
|
|
@ -40,7 +40,6 @@ import { fileURLToPath } from 'node:url'
|
|||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO = resolve(HERE, '../../..')
|
||||
const T = (f) => JSON.parse(readFileSync(resolve(REPO, 'data/tables', f), 'utf8'))
|
||||
const BOUND = (f) => JSON.parse(readFileSync(resolve(REPO, 'public/dx-maps/assets/boundaries', f), 'utf8'))
|
||||
const OUTS = [
|
||||
resolve(REPO, 'data/tables/gospel-internet.json'),
|
||||
resolve(REPO, 'public/dx-maps/assets/tables/gospel-internet.json'),
|
||||
|
|
@ -56,6 +55,7 @@ const conn = T('country-connectivity.json')
|
|||
const ibc = T('internet-by-country.json')
|
||||
const adm1 = T('internet-by-adm1.json')
|
||||
const byProvince = T('gospel-by-province.json')
|
||||
const byGroup = T('gospel-by-province-groups.json')
|
||||
|
||||
// ── join 1: access <-> roster on name + iso3 (the key the spec measured at 2,107 / 2,107) ──────────
|
||||
const key = (name, iso3) => `${name}|${iso3}`
|
||||
|
|
@ -86,36 +86,17 @@ for (const r of adm1.rows) {
|
|||
adm1By.set(k, a)
|
||||
}
|
||||
|
||||
// ── province, by point-in-polygon on the files gospel-by-province reads ────────────────────────────
|
||||
const ringHas = (ring, x, y) => {
|
||||
let inside = false
|
||||
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
||||
const xi = ring[i][0], yi = ring[i][1], xj = ring[j][0], yj = ring[j][1]
|
||||
if (((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi)) inside = !inside
|
||||
}
|
||||
return inside
|
||||
}
|
||||
const polyHas = (poly, x, y) => ringHas(poly[0], x, y) && !poly.slice(1).some((h) => ringHas(h, x, y))
|
||||
const featHas = (g, x, y) => g?.type === 'Polygon' ? polyHas(g.coordinates, x, y)
|
||||
: g?.type === 'MultiPolygon' ? g.coordinates.some((p) => polyHas(p, x, y)) : false
|
||||
const bboxOf = (g) => {
|
||||
const m = [180, 90, -180, -90]
|
||||
const walk = (a) => { if (typeof a[0] === 'number') { m[0] = Math.min(m[0], a[0]); m[1] = Math.min(m[1], a[1]); m[2] = Math.max(m[2], a[0]); m[3] = Math.max(m[3], a[1]) } else a.forEach(walk) }
|
||||
walk(g.coordinates)
|
||||
return m
|
||||
}
|
||||
const PROVINCE_FILES = { IND: 'gb-IND-ADM1-full.geojson', CHN: 'gb-CHN-ADM1.geojson' }
|
||||
const polys = {}
|
||||
for (const [iso3, f] of Object.entries(PROVINCE_FILES)) {
|
||||
polys[iso3] = BOUND(f).features.map((ft) => ({ name: ft.properties.shapeName, g: ft.geometry, b: bboxOf(ft.geometry) }))
|
||||
}
|
||||
const provinceOf = (iso3, lng, lat) => {
|
||||
for (const c of polys[iso3] || []) {
|
||||
if (lng < c.b[0] || lng > c.b[2] || lat < c.b[1] || lat > c.b[3]) continue
|
||||
if (featHas(c.g, lng, lat)) return c.name
|
||||
}
|
||||
return ''
|
||||
// ── province: gospel-plugin's per-group assignment, keyed like join 1 (name + iso3) ─────────────────
|
||||
const PROVINCE_COUNTRIES = new Set(['IND', 'CHN'])
|
||||
const provinceBy = new Map()
|
||||
for (const g of byGroup.rows || []) {
|
||||
const k = key(g.people_group, g.iso3)
|
||||
if (provinceBy.has(k)) fail(`gospel-by-province-groups key duplicated: ${k}`)
|
||||
provinceBy.set(k, g.province)
|
||||
}
|
||||
const aggSum = (byProvince.rows || []).reduce((n, p) => n + (p.groups || 0), 0)
|
||||
if (aggSum !== provinceBy.size) fail(`gospel-by-province sums to ${aggSum} groups but gospel-by-province-groups has ${provinceBy.size}: one file is stale`)
|
||||
const unassignedKeys = new Set((byProvince.unassigned || []).map((u) => key(u.people_group, u.iso3)))
|
||||
|
||||
// ── how many groups share each coordinate (the honest pin notice counts them) ──────────────────────
|
||||
const coordCount = new Map()
|
||||
|
|
@ -145,7 +126,10 @@ for (const a of access.rows) {
|
|||
a.scripture_url && 'scripture', a.film_url && 'film', (a.audio_url || a.gospel_audio_url) && 'audio',
|
||||
a.bible_video_url && 'video', a.stories_url && 'stories',
|
||||
].filter(Boolean)
|
||||
const province = PROVINCE_FILES[a.iso3] ? provinceOf(a.iso3, Number(r.lng), Number(r.lat)) : ''
|
||||
const province = PROVINCE_COUNTRIES.has(a.iso3) ? (provinceBy.get(key(a.people_group, a.iso3)) || '') : ''
|
||||
if (PROVINCE_COUNTRIES.has(a.iso3) && !province && !unassignedKeys.has(key(a.people_group, a.iso3))) {
|
||||
fail(`${a.people_group} (${a.iso3}) is neither placed in a province nor listed as unassigned`)
|
||||
}
|
||||
const st = province && a.iso3 === 'IND' ? adm1By.get(`IND|${province}`) : null
|
||||
rows.push({
|
||||
slug: r.slug,
|
||||
|
|
@ -192,13 +176,8 @@ for (const a of access.rows) {
|
|||
|
||||
// ── coverage: every group in, and the province placement agrees with gospel-by-province ────────────
|
||||
if (rows.length !== access.rows.length) fail(`rows ${rows.length} != access rows ${access.rows.length}`)
|
||||
const mine = new Map()
|
||||
for (const r of rows) if (r.province) mine.set(`${r.iso3}|${r.province}`, (mine.get(`${r.iso3}|${r.province}`) || 0) + 1)
|
||||
const theirs = new Map(byProvince.rows.map((p) => [`${p.iso3}|${p.province}`, p.groups]))
|
||||
for (const [k, n] of theirs) if (mine.get(k) !== n) fail(`province ${k}: this build places ${mine.get(k) || 0}, gospel-by-province says ${n}`)
|
||||
for (const [k, n] of mine) if (!theirs.has(k)) fail(`province ${k}: ${n} groups here, absent from gospel-by-province`)
|
||||
const unplaced = rows.filter((r) => PROVINCE_FILES[r.iso3] && !r.province)
|
||||
if (unplaced.length !== byProvince.unassigned.length) fail(`unplaced IND/CHN groups ${unplaced.length} != gospel-by-province unassigned ${byProvince.unassigned.length}`)
|
||||
const unplaced = rows.filter((r) => PROVINCE_COUNTRIES.has(r.iso3) && !r.province)
|
||||
if (rows.filter((r) => r.province).length !== provinceBy.size) fail(`placed ${rows.filter((r) => r.province).length} groups but gospel-by-province-groups has ${provinceBy.size}`)
|
||||
|
||||
const count = (f) => rows.reduce((m, r) => { m[r[f]] = (m[r[f]] || 0) + 1; return m }, {})
|
||||
const zeroOwn = rows.filter((r) => r.access_detail === 'none')
|
||||
|
|
@ -228,12 +207,12 @@ if (failures.length) {
|
|||
inputs_generated: {
|
||||
'doxa-gospel-access': access.meta?.generated || '', 'doxa-people-groups': roster.generated || '',
|
||||
'country-connectivity': conn.meta?.source?.accessed || '', 'internet-by-country': ibc.stats?.generated || '',
|
||||
'gospel-by-province': byProvince.meta?.generated || '',
|
||||
'gospel-by-province': byProvince.meta?.generated || '', 'gospel-by-province-groups': byGroup.meta?.generated || '',
|
||||
},
|
||||
coverage,
|
||||
band_rule: bandingText,
|
||||
internet_note: 'internet_pct is the COUNTRY\'s figure (World Bank), never the group\'s. state_internet_* is India\'s state figure from NFHS-5 (2019-21): the share of women and of men aged 15-49 who have EVER used the internet, kept apart, never averaged, never on a scale with subscriptions per 100.',
|
||||
province_note: 'province is placed by point-in-polygon on the group\'s coordinate, which marks where a PEOPLE is found, not a town. It is approximate. Placement agrees group-for-group with gospel-by-province.json (checked at build).',
|
||||
province_note: 'province is read from gospel-by-province-groups.json (gospel-plugin): placed by point-in-polygon on the group\'s coordinate, which marks where a PEOPLE is found, not a town. It is approximate, and near a border it can fall on the wrong side; it is never a fact about connectivity in that state.',
|
||||
access_note: 'access_detail says what exists in the group\'s OWN language in the sources checked; "none" means nothing was found there, never that nothing exists. access_level: own = something in their language; bridge = nothing in their language but a bridge, candidate or Deaf way in; zero = neither.',
|
||||
never_read: 'engagement, workers, teams, agencies: who works where is not shown (a safety default, pending the Driver).',
|
||||
// Rows omit empty fields ('' / null / false / a zero shares_point) to keep the fetch small for
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue