From 87c36e4856d31ad51793d67ba3201d0495313ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Sun, 19 Jul 2026 23:05:27 -0300 Subject: [PATCH] Deliver A8 IBGE voter profile on Eleitorado tab with post-simplify scale plan. Adds static SIDRA demographics, territory-weighted derivation, electorate UI with opt-in copy to manual profiles, shared territory-city resolution, and registers A8+ fill-in debt from capture-review-debts. --- .gitignore | 2 + docs/plans/escala-dry-pos-a8.md | 117 + docs/plans/escala-dry-pos-b2.md | 7 +- docs/plans/perfil-eleitorado-ibge.md | 66 +- docs/roadmap.md | 8 +- package.json | 1 + scripts/build-bahia-demographics.mjs | 299 + .../nucleusIbgeVoterProfileFormActions.ts | 95 + src/components/campaign/NucleusActiveTab.tsx | 39 +- .../campaign/NucleusElectorateTab.tsx | 69 + .../campaign/NucleusIbgeVoterProfile.tsx | 88 + .../campaign/NucleusIntelligenceDialog.tsx | 15 +- .../campaign/NucleusVoterProfileCard.tsx | 42 + src/lib/bahiaMunicipalityDemographics.ts | 487 ++ src/utilities/nucleusElectionGeography.ts | 21 +- src/utilities/nucleusIbgeVoterProfile.ts | 125 + src/utilities/nucleusIntelligenceUi.ts | 43 + src/utilities/nucleusTerritoryCities.ts | 24 + ...ia-municipality-demographics.official.json | 5010 +++++++++++++++++ .../bahiaMunicipalityDemographics.int.spec.ts | 102 + .../unit/nucleusIbgeVoterProfile.unit.spec.ts | 53 + 21 files changed, 6621 insertions(+), 92 deletions(-) create mode 100644 docs/plans/escala-dry-pos-a8.md create mode 100644 scripts/build-bahia-demographics.mjs create mode 100644 src/app/(campaign)/campanha/(app)/nucleos/[slug]/nucleusIbgeVoterProfileFormActions.ts create mode 100644 src/components/campaign/NucleusElectorateTab.tsx create mode 100644 src/components/campaign/NucleusIbgeVoterProfile.tsx create mode 100644 src/components/campaign/NucleusVoterProfileCard.tsx create mode 100644 src/lib/bahiaMunicipalityDemographics.ts create mode 100644 src/utilities/nucleusIbgeVoterProfile.ts create mode 100644 src/utilities/nucleusTerritoryCities.ts create mode 100644 tests/fixtures/bahia-municipality-demographics.official.json create mode 100644 tests/int/bahiaMunicipalityDemographics.int.spec.ts create mode 100644 tests/unit/nucleusIbgeVoterProfile.unit.spec.ts diff --git a/.gitignore b/.gitignore index 09b56eb0..91dcbd2a 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,8 @@ next-env.d.ts # IBGE geometry download cache (pnpm build:geometries) /data/geometries/ +# IBGE demographics download cache (pnpm build:demographics) +/data/demographics/ # Playwright node_modules/ diff --git a/docs/plans/escala-dry-pos-a8.md b/docs/plans/escala-dry-pos-a8.md new file mode 100644 index 00000000..ce8b71b7 --- /dev/null +++ b/docs/plans/escala-dry-pos-a8.md @@ -0,0 +1,117 @@ +# Escala e DRY pós-A8 (perfil IBGE Eleitorado) + +Status: Fases 1–3 pendentes (engenharia A8 em implementação no branch) +Atualizado em: 2026-07-20 (`capture-review-debts` pós-dois `/simplify` na entrega A8) +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha A, fill-in **A8+** pós-A8) +Impeccable: A — N/A (sem superfície UI; perf/integridade de dado) +Appetite: ~0,5–1 dia eng (3 fases pequenas; PR único ou 2 PRs) +Responsável: — + +## Contexto + +O A8 ([perfil-eleitorado-ibge.md](perfil-eleitorado-ibge.md)) entrega artefato estático `bahiaMunicipalityDemographics.ts`, derivação `getNucleusIbgeVoterProfile`, aba Eleitorado com dynamic import de `NucleusElectorateTab`, e opt-in “Usar como perfil manual”. Duas passagens `/simplify` (pré e pós-rebase em `main` com A7 F2) já limparam o que cabia em cleanup: `safeMessages` + `router.refresh`, `NucleusVoterProfileCard`, helpers `voterProfileNullsToUndefined` / `normalizedVoterProfilesForWrite`, `resolveNucleusTerritoryCities` compartilhado com `nucleusElectionGeography`, JSDoc de zone rules restaurado, formatters `Intl` em cache. + +Os revisores (performance / reuse / quality) marcaram como **maiores que cleanup** os follow-ups abaixo. Sem registro, o cold start da aba Eleitorado e da server action de cópia continuam pagando parse do módulo ~68KB, e núcleos com município inválido na lista podem exibir perfil parcial sem aviso. + +**Já resolvido no simplify/critique (não reabrir):** form action com `updateNucleus` + `CampaignFormActionState`; lazy import da aba; DRY card manual + normalização de write; `toNucleusElectionGeographyInput` na leitura; `NucleusElectionGeographyInput` unificado com `NucleusTerritoryCitiesInput`; `export` removido de `AGE_BAND_CATEGORY_IDS` no script; testes unit/int A8 verdes. + +## Objetivos + +- Cold start da aba Eleitorado e da action de cópia não avaliam `bahiaMunicipalityDemographics.ts` até `getNucleusIbgeVoterProfile` ser chamado (padrão B5 F1 / lazy geometries). +- Agregação IBGE **não mente por omissão**: quando `matchedCities < effectiveCities`, o produto falha fechado ou sinaliza cobertura parcial de forma explícita na UI — sem PII, sem migration. +- Artefato demographics versionado ganha soft budget de bytes nos testes int (regressão de codegen), espelhando B2 geometries. +- Guardrails: sem migration, sem collection, sem Consent, sem HTTP SIDRA em runtime; access inalterado (staff-only). + +## Decisões travadas + +- **Um fill-in A8+, três fases ordenadas.** Mesmo racional de A7/C6/B5: um slug de plano, PRs por fase. Ordem: lazy load (impacto cold start) → integridade de cobertura (produto) → size budget (higiene). **Rejeitado:** item de roadmap separado `A9` — follow-up pós-entrega, não feature nova. +- **Dependência dura de A8.** Só faz sentido com `bahiaMunicipalityDemographics.ts`, `getNucleusIbgeVoterProfile` e aba Eleitorado já mergeados; não reabre escopo SIDRA/renda/RAIS. +- **Lazy load no deriver, não no componente.** Dynamic import da aba já isola rotas não-Eleitorado; F1 move o custo do módulo estático para dentro de `getNucleusIbgeVoterProfile` (e callers async se necessário). **Rejeitado:** segundo dynamic import só no card IBGE — duplicaria fronteira async. +- **Cortável pós-propaganda.** Fill-in de qualidade/perf; não bloqueia Onda 0 nem propaganda 16/08 se A8 principal já estiver em prod. +- **Cache CLI do build demographics → B5 F2**, não este plano. `scripts/build-bahia-demographics.mjs` é 4º clone de `ensureCached*`; absorvido em [escala-dry-pos-b2.md](escala-dry-pos-b2.md). +- **i18n e naming** (AGENTS.md): identificadores em inglês (`loadDemographicsModule`, `demographicsCoverageStatus`); strings visíveis em pt-BR só na UI de aviso de cobertura parcial. + +## Questões em aberto + +- **Fail-closed vs aviso parcial na F2?** **Opções:** (A) `status: 'semPerfil'` quando qualquer cidade falha; (B) `available` com flag `partialCoverage` + copy na UI; (C) aviso só em `notes`. **Recomendação:** (B) — coordenação ainda vê o perfil ponderado dos municípios válidos, com badge/linha “Cobertura parcial (N de M municípios)”; evita empty state enganoso sem esconder dado útil. +- **`getNucleusIbgeVoterProfile` sync ou async após F1?** **Opções:** função `async` sempre; loader interno com `await import()` e API sync que lança se módulo não carregado. **Recomendação:** `async` + `await getNucleusIbgeVoterProfile` em `NucleusElectorateTab` (server) e na form action — poucos call sites, sem Promise em cada render de card. + +## Abordagem proposta + +```mermaid +flowchart TD + A8["A8 perfil IBGE ✓/em merge"] --> F1 + F1["Fase 1 — lazy bahiaMunicipalityDemographics"] + F1 --> F2["Fase 2 — cobertura parcial explícita"] + F2 --> F3["Fase 3 — size budget int"] + B5F2["B5 F2 ensureCachedDownload"] -.4º script.-> DemoCLI["build-bahia-demographics.mjs"] +``` + +### Fase 1 — Lazy load do artefato demographics + +- Introduzir `loadBahiaMunicipalityDemographicsModule()` (ou dynamic `import('@/lib/bahiaMunicipalityDemographics')` cacheado em `let modulePromise`) consumido por `getNucleusIbgeVoterProfile`. +- Tornar `getNucleusIbgeVoterProfile` **async**; atualizar `NucleusElectorateTab`, `nucleusIbgeVoterProfileFormActions.ts` e testes unit. +- Critério: import estático de `NucleusActiveTab` / rotas não-Eleitorado não puxam `bahiaMunicipalityDemographics.ts` no grafo (inspeção de bundle ou análise de imports). + +### Fase 2 — Cobertura parcial explícita + +- Contar `matchedCities` vs `effectiveCities` em `getNucleusIbgeVoterProfile`. +- Se `matchedCities === 0` → `{ status: 'semPerfil' }` (inalterado). +- Se `matchedCities < effectiveCities` → `available` com metadado de cobertura (`partialCoverage: true`, contagens) e copy pt-BR na UI (`NucleusIbgeVoterProfile` ou view model). +- Testes unit: cidade inválida no meio de duas válidas → parcial; todas inválidas → `semPerfil`. + +### Fase 3 — Size budget no artefato + +- Teste int em `tests/int/bahiaMunicipalityDemographics.int.spec.ts`: `stat` de `src/lib/bahiaMunicipalityDemographics.ts` ≤ ~100 KB (margem sobre ~68 KB atuais). +- Sem mudança de comportamento de runtime. + +**Migration:** nenhuma nas três fases. + +## Dependências + +- **Dura:** A8 [perfil-eleitorado-ibge.md](perfil-eleitorado-ibge.md) — artefato + derivação + aba (merge em `main`). +- **Suave:** B5 F1 lazy geometries — precedente de padrão, não bloqueia. +- Reusa: `resolveNucleusTerritoryCities`, `NucleusElectorateTab` dynamic import, `tests/int/bahiaGeometries.int.spec.ts` (modelo de size budget). + +## Não escopo + +- HTTP SIDRA em request path → rabbit hole A8 (perfil-eleitorado-ibge.md). +- Pré-agregar demographics por TI no build → defer no plano-pai A8 (gatilho: region-only lento). +- Renomear `semPerfil` para inglês → higiene score 1; descartado no triage. +- `Promise.all` nos fetches do build script → só build-time; descartado. +- Helper CLI `ensureCachedDownload` → **B5 F2** ([escala-dry-pos-b2.md](escala-dry-pos-b2.md)). +- Impeccable polish classe B da aba → defer no plano-pai até marcar A8 entregue em prod. +- Alinhar semântica IBGE vs mapa TSE (cidades vs zonas) → produto/documentação no plano-pai, não engenharia neste fill-in. + +## Rabbit holes + +- **Re-exportar demographics no barrel `bahiaGeometries` ou entry de campanha.** Mitigação: import lazy **só** dentro de `nucleusIbgeVoterProfile.ts`. +- **Collection Payload “só para cachear demographics”.** Mitigação: manter artefato estático versionado (decisão A8). +- **Unificar perfil IBGE com baseline TSE (A3/A4).** Mitigação: fontes distintas; sem join neste fill-in. + +## Adiado com gatilho + +- **Pré-agregação demographics por TI no build.** Revisitar quando núcleos region-only com >15 municípios mostrarem latência perceptível em prod. +- **`formatPercent` compartilhado cross-codebase.** Revisitar no 3º consumer além de IBGE + insights. +- **Testes unit dedicados a `resolveNucleusTerritoryCities`.** Revisitar no 3º consumer (hoje: election geography + IBGE). +- **Semântica IBGE (todas as cidades) vs mapa (geografia TSE).** Documentado no plano-pai; revisitar se usuários reportarem footprint vazio com perfil IBGE visível. + +## Explicitamente fora (triage `capture-review-debts` 2026-07-20) + +- Renomear `semPerfil` → `noProfile` (pureza de naming). +- `Promise.all` paralelo nos 28 fetches SIDRA do build (só CLI). +- Harmonizar `revalidatePath` slug vs `[slug]` page entre form actions. +- Impeccable critique/polish da aba Eleitorado antes de prod (defer no plano-pai A8). + +## Referências + +- `docs/roadmap.md` (Trilha A, A8 + fill-in A8+) +- `docs/plans/perfil-eleitorado-ibge.md` — plano pai A8 +- `docs/plans/escala-dry-pos-b2.md` — B5 F2 cache CLI (4º call site `build-bahia-demographics.mjs`) +- `src/lib/bahiaMunicipalityDemographics.ts` — artefato ~68KB +- `src/utilities/nucleusIbgeVoterProfile.ts` — derivação +- `src/components/campaign/NucleusElectorateTab.tsx` — consumidor server +- `src/app/(campaign)/campanha/(app)/nucleos/[slug]/nucleusIbgeVoterProfileFormActions.ts` — action de cópia +- `scripts/build-bahia-demographics.mjs` — CLI (cache → B5 F2) +- `tests/int/bahiaGeometries.int.spec.ts` — precedente size budget +- AGENTS.md — naming EN/pt-BR; dado estático B2; sem PII diff --git a/docs/plans/escala-dry-pos-b2.md b/docs/plans/escala-dry-pos-b2.md index 64e48d52..053a5e8c 100644 --- a/docs/plans/escala-dry-pos-b2.md +++ b/docs/plans/escala-dry-pos-b2.md @@ -1,7 +1,7 @@ # Escala e DRY pós-B2 (geometrias + scripts CLI) Status: Fase 1 entregue com B3 (2026-07-19); Fases 2–3 pendentes -Atualizado em: 2026-07-20 (nota cruzada `capture-review-debts` pós-A7 F2: `build-bahia-tse-city-codes.mjs` reforça gatilho F2) +Atualizado em: 2026-07-20 (`capture-review-debts` pós-A8: `build-bahia-demographics.mjs` reforça gatilho F2 como 4º call site) Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B5) Responsável: — @@ -10,7 +10,7 @@ Responsável: — O B2 ([mapa-bahia-geometrias.md](mapa-bahia-geometrias.md) Fase 1) entregou a fundação estática do mapa: `*.topo.json` de municípios e Territórios de Identidade, `bahiaMunicipalityCodes`, helpers em `bahiaGeometries.ts`, script `pnpm build:geometries`. A passagem `/simplify` de 2026-07-18 limpou dead code e churns baratos no script/codegen, mas **deixou de fora** dois follow-ups que os revisores (performance / reuse) marcaram como importantes e maiores que cleanup pontual: 1. **Carregamento eager de geometrias no cliente.** `src/lib/bahiaGeometries.ts` importa os dois TopoJSON, roda `topojson-client` `feature()` em 417+27 polígonos e monta índices no load do módulo — custo de bundle/memória antes de qualquer superfície Leaflet (B3). -2. **Cache de download duplicado nos CLIs.** `ensureCachedJson` em `scripts/build-bahia-geometries.mjs` é estruturalmente o mesmo que `ensureCachedZip` em `scripts/seed-tse-results.mjs` (mkdir → hit → `downloadToBuffer` → SHA-256 → write), só muda extensão/parse. **Pós-A7 F2:** `scripts/build-bahia-tse-city-codes.mjs` (`pnpm build:tse-city-codes`) clona o mesmo padrão `ensureCachedZip` — terceiro call site, reforça prioridade da Fase 2 deste plano sem novo ID. +2. **Cache de download duplicado nos CLIs.** `ensureCachedJson` em `scripts/build-bahia-geometries.mjs` é estruturalmente o mesmo que `ensureCachedZip` em `scripts/seed-tse-results.mjs` (mkdir → hit → `downloadToBuffer` → SHA-256 → write), só muda extensão/parse. **Pós-A7 F2:** `scripts/build-bahia-tse-city-codes.mjs` (`pnpm build:tse-city-codes`) clona o mesmo padrão `ensureCachedZip` — terceiro call site. **Pós-A8 (`capture-review-debts` 2026-07-20):** `scripts/build-bahia-demographics.mjs` (`pnpm build:demographics`) clona `ensureCachedJson` — quarto call site; reforça prioridade da Fase 2 sem novo ID de roadmap. 3. **Factory duplicada mun/TI pós-B3 F1.** `bahiaMunicipalityGeometries.ts` e `bahiaTerritoryGeometries.ts` repetem o mesmo pipeline `topojson.feature` → array → `Map` por chave → export do módulo indexado — só mudam topology object, propriedade de chave (`codarea` vs `code`) e getter. Este plano é o registro canônico desses follow-ups. Sem ele, o B3 pode embarcar o mapa com payload desnecessário no path default, e o próximo script de dado aberto volta a clonar o cache. @@ -63,7 +63,7 @@ flowchart TD - Extrair `ensureCachedDownload({ cacheDir, key, url, ext?, logPrefix })` a partir de `ensureCachedJson` / `ensureCachedZip`. - Retorno: `{ url, buffer, hash }` (parse JSON/ZIP fica no caller — `build-bahia-geometries` já faz `JSON.parse`; seed TSE usa `readZipEntry`). - Continuar usando `downloadToBuffer` (`src/lib/electionResultsZip.ts`). -- Migrar `scripts/build-bahia-geometries.mjs`, `scripts/seed-tse-results.mjs` e `scripts/build-bahia-tse-city-codes.mjs`; sem mudança de comportamento nem de pastas `data/geometries/` / `data/tse/`. +- Migrar `scripts/build-bahia-geometries.mjs`, `scripts/seed-tse-results.mjs`, `scripts/build-bahia-tse-city-codes.mjs` e `scripts/build-bahia-demographics.mjs`; sem mudança de comportamento nem de pastas `data/geometries/` / `data/tse/` / `data/demographics/`. ### Fase 3 — Factory compartilhada mun/TI @@ -98,6 +98,7 @@ flowchart TD - `src/lib/bahiaMunicipalityGeometries.ts` / `bahiaTerritoryGeometries.ts` — near-duplicate pós-F1 - `src/lib/bahiaGeometriesTypes.ts` — tipos `*GeometryModule` - `scripts/build-bahia-geometries.mjs` — `ensureCachedJson` +- `scripts/build-bahia-demographics.mjs` — `ensureCachedJson` (4º call site; A8) - `scripts/seed-tse-results.mjs` — `ensureCachedZip` - `src/lib/electionResultsZip.ts` — `downloadToBuffer` - AGENTS.md — naming inglês; Bahia implícita; dado estático versionado diff --git a/docs/plans/perfil-eleitorado-ibge.md b/docs/plans/perfil-eleitorado-ibge.md index 50dfa59a..79b63657 100644 --- a/docs/plans/perfil-eleitorado-ibge.md +++ b/docs/plans/perfil-eleitorado-ibge.md @@ -1,9 +1,11 @@ # Perfis do eleitorado — perfil médio IBGE + manuais -Status: rascunho +Status: em implementação Atualizado em: 2026-07-19 Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha A, item A8) Responsável: — +Appetite: ~1–1,5 dia eng (artefato estático + derivação + UI encaixe + opt-in de cópia) +Impeccable: classe B (UI encaixada na aba Eleitorado existente) ## Contexto @@ -15,33 +17,29 @@ Já temos o join estável município → código IBGE 7 dígitos em `src/lib/bah ## Objetivos -- Importar (CLI, estático) indicadores demográficos municipais BA suficientes para montar um “perfil médio” legível (idade dominante, sexo, renda, escolaridade/ocupação quando a tabela cobrir). +- Importar (CLI, estático) indicadores demográficos municipais BA suficientes para montar um “perfil médio” legível (faixa etária dominante, sexo, idade mediana). - Derivar em leitura o perfil do núcleo a partir de `cities[]` (média ponderada pela população quando houver >1 município; só `regions` → união das cidades do TI via `citiesForTerritory`). - Exibir o perfil calculado na aba Eleitorado, claramente marcado como **Calculado (IBGE)**, separado dos manuais. -- Manter CRUD de `voterProfiles` manuais; opcionalmente oferecer “Adicionar como perfil” (copia o calculado para o array, editável depois). +- Manter CRUD de `voterProfiles` manuais; botão opt-in “Usar como perfil manual” (copia o calculado para o array, editável depois). - Guardrails: dado público agregado (sem PII, sem `Consent`); access só staff (mesmo escopo da aba Eleitorado hoje); sem chamada HTTP ao IBGE em runtime de request; `overrideAccess: false` nas leituras de núcleo. ## Decisões travadas - **Perfil calculado é derivado em leitura, não sobrescreve `voterProfiles`.** Persistir no array misturaria fonte IBGE com edição humana e apagaria trabalho do coordenador em re-seed. O cartão IBGE é view model; o array continua 100% manual. -- **Dado estático versionado no repo** (padrão B2/`bahiaTseZones`/`bahiaMunicipalityCodes`), não collection Payload nem fetch SIDRA por request. Script re-executável baixa Agregados/SIDRA (ou CSV oficial), emite `src/lib/bahiaMunicipalityDemographics.ts` (+ fixture). Sem migration, sem `Consent`, sem revalidate (páginas de campanha dinâmicas). +- **Dado estático versionado no repo** (padrão B2/`bahiaTseZones`/`bahiaMunicipalityCodes`), não collection Payload nem fetch SIDRA por request. Script re-executável baixa SIDRA, emite `src/lib/bahiaMunicipalityDemographics.ts` (+ fixture). Sem migration, sem `Consent`, sem revalidate (páginas de campanha dinâmicas). - **Granularidade município.** Núcleo indexa `cities[]`; bairro/seção não têm série IBGE estável alinhada ao app. Multi-cidade = ponderação por população residente do Censo. - **Sem geografia → estado “sem perfil IBGE”** (mesmo espírito do baseline TSE sem território). -- **v1 cobre BA, Censo 2022** (e tabelas de rendimento/trabalho já divulgadas no mesmo ciclo). Séries antigas (2010) e PNAD contínua ficam fora. +- **v1 cobre BA, Censo 2022** — tabelas SIDRA **9514** (população por sexo/idade) + **9515** (idade mediana). Faixas agregadas: `0–17` / `18–29` / `30–59` / `60+`. `incomeBand` / `occupation` / cor-raça **fora** até haver tabela municipal estável no Agregados. +- **Staff-only na v1** — aba Eleitorado continua redirecionando `lideranca` para overview. +- **UI:** números + 1 linha de síntese template (constantes versionadas), sem LLM. +- **Copiar para `voterProfiles`:** botão opt-in “Usar como perfil manual” via Server Action de append; nunca auto-append no load. - **i18n e naming** (AGENTS.md): identificadores em inglês (`bahiaMunicipalityDemographics`, `getNucleusIbgeVoterProfile`, `ComputedVoterProfileViewModel`); strings visíveis em pt-BR (“Perfil médio do território”, “Calculado (IBGE Censo 2022)”). -## Questões em aberto - -- **Quais indicadores entram no cartão v1?** Faixa etária modal + % sexo + renda domiciliar per capita (ou faixa) + cor/raça dominante são o mínimo útil e mapam bem aos campos já existentes (`ageRange`/`incomeBand`). Ocupação formal (RAIS) é outra fonte. **Recomendação:** v1 = população + pirâmide etária resumida (3–4 faixas) + sexo + rendimento; `occupation`/`localTraits` só no manual ou numa linha “destaque” textual gerada se houver variável estável; definir tabelas SIDRA exatas na implementação e documentar no cabeçalho do módulo. -- **`lideranca` vê o perfil IBGE?** Hoje a aba Eleitorado redireciona `lideranca` para overview. **Recomendação:** manter staff-only na v1 (dado agregado público, mas a superfície de inteligência já é staff); reavaliar com produto se quiserem kit de campo para liderança. -- **Texto gerado vs. só números?** Um parágrafo (“Predominam adultos 30–59, renda …”) ajuda o empty state; números sozinhos evitam tom “científico” falso. **Recomendação:** números + 1 linha de síntese template (constantes versionadas), sem LLM. -- **Copiar para `voterProfiles`?** **Recomendação:** botão opt-in “Usar como perfil manual” na aba (Server Action que faz append de um item pré-preenchido); nunca auto-append no load. - ## Abordagem proposta ```mermaid flowchart LR - SIDRA["IBGE Agregados / SIDRA
Censo 2022 BA N6"] + SIDRA["IBGE SIDRA
9514 + 9515 N6 BA"] Script["scripts/build-bahia-demographics.mjs
pnpm build:demographics"] Static["bahiaMunicipalityDemographics.ts
codarea → indicadores"] Nuc["electoralNucleus.cities[]"] @@ -57,19 +55,35 @@ flowchart LR Componentes: -- **`bahiaMunicipalityDemographics`** (`src/lib/bahiaMunicipalityDemographics.ts`): mapa `codarea` → `{ population, ageBands, sexShare, incomeBand, … }` + helpers `demographicsForCode` / `demographicsForMunicipalityName`. Cabeçalho com URL + SHA-256 dos downloads (padrão `bahiaMunicipalityCodes`). -- **`getNucleusIbgeVoterProfile`** (`src/utilities/nucleusIbgeVoterProfile.ts`, `server-only`): entrada `{ cities, regions }` → `ComputedVoterProfileViewModel | { status: 'semPerfil' }`. Resolve cidades efetivas (`cities` ou `citiesForTerritory` de `bahiaTerritories.ts`); pondera indicadores; formata `label` fixo (“Perfil médio do território”) + campos alinhados ao schema manual para a UI e para o opt-in de cópia. -- **`ElectorateContent`** (`NucleusActiveTab.tsx`): acima dos manuais, card `NucleusIbgeVoterProfile` (novo, PascalCase) com badge “Calculado (IBGE)”; empty state só quando não há calculado **nem** manuais; atualizar copy (“nenhum perfil cadastrado” → distinguir ausência de IBGE vs. ausência de manuais). -- **`NucleusIntelligenceDialog`**: sem mudança obrigatória no schema; opcionalmente botão/ação “Adicionar perfil a partir do IBGE” que pré-preenche um `EditableProfile` no estado cliente (ou Server Action de append). -- **Script** `scripts/build-bahia-demographics.mjs` (`pnpm build:demographics`): baixa Agregados para os 417 municípios BA; reusa `downloadToBuffer` / cache sob `data/demographics/` (gitignored); **não toca banco** (sem `assertLocalDatabase`). Fora de `pnpm build` / `pnpm dev`. -- **Testes:** unit do ponderador (1 cidade, 2 cidades, sem geografia, município sem row); int de cobertura 417 `codarea` vs `bahiaMunicipalityCodes` + fixture `tests/fixtures/bahia-municipality-demographics.official.json`. -- **Migration:** nenhuma. Sem collection nova. Sem Consent. Server Action só se o opt-in de cópia for mutação dedicada (senão reusa o update de inteligência existente). +- **`bahiaMunicipalityDemographics`** (`src/lib/bahiaMunicipalityDemographics.ts`): mapa `codarea` → `{ population, ageBands, sexShareFemale, medianAge }` + helpers `demographicsForCode` / `demographicsForMunicipalityName`. Cabeçalho com URL + SHA-256 dos downloads (padrão `bahiaMunicipalityCodes`). +- **`getNucleusIbgeVoterProfile`** (`src/utilities/nucleusIbgeVoterProfile.ts`): entrada `{ cities, regions }` → `ComputedVoterProfileViewModel | { status: 'semPerfil' }`. Resolve cidades efetivas (`cities` ou `citiesForTerritory`); pondera indicadores; formata `label` fixo (“Perfil médio do território”) + campos alinhados ao schema manual para a UI e para o opt-in de cópia. +- **`ElectorateContent`** (`NucleusActiveTab.tsx`): acima dos manuais, card `NucleusIbgeVoterProfile` com badge “Calculado (IBGE)”; empty state só quando não há calculado **nem** manuais. +- **Script** `scripts/build-bahia-demographics.mjs` (`pnpm build:demographics`): baixa SIDRA para os 417 municípios BA; cache `data/demographics/` (gitignored); **não toca banco**. Fora de `pnpm build` / `pnpm dev`. +- **Testes:** unit do ponderador; int de cobertura 417 `codarea` vs `bahiaMunicipalityCodes` + fixture `tests/fixtures/bahia-municipality-demographics.official.json`. +- **Migration:** nenhuma. + +## Rabbit holes (não tocar) + +- HTTP SIDRA em request path +- Microdados / perfilamento individual +- Abrir Eleitorado para `lideranca` +- Camada demográfica no mapa Leaflet (B3/B4) +- Acoplar loaders A3/A4/TSE +- Collection Payload / migration “só por precaução” +- RAIS/PNAD / Censo 2010 / renda sem tabela municipal estável + +## Adiado com gatilho + +- **Pré-agregar demographics por TI no build** (`demographicsForTerritory`). Revisitar quando núcleos só-`regions` com >15 municípios mostrarem latência perceptível em prod. +- **Semântica IBGE vs mapa TSE.** Perfil IBGE usa todas as cidades do território (`resolveNucleusTerritoryCities`); o coroplético/mapa usa geografia TSE com zonas (`resolveNucleusElectionGeography`) — footprint vazio com perfil IBGE visível é possível. Revisitar se coordenação reportar confusão; copy de empty state ou nota na UI, não mudança de fonte. +- **Impeccable polish classe B** (shape/critique/polish da aba Eleitorado). Revisitar antes de marcar A8 entregue em prod com dados reais. +- **Follow-ups de engenharia pós-`/simplify`** (lazy load ~68KB, cobertura parcial, size budget) → [escala-dry-pos-a8.md](escala-dry-pos-a8.md) (fill-in **A8+**). ## Dependências - **Dura:** A1 Território multi-município/bairro ✓ — `cities[]` / `regions[]` e `citiesForTerritory`. -- **Suave:** B2 ✓ — `bahiaMunicipalityCodes` / `codeForMunicipality` (sem isso o script teria que reimplementar o join nome→`codarea`). -- **Nenhuma dependência de A3/A4** — fonte IBGE ≠ TSE; o baseline de votos e o perfil demográfico convivem na mesma ficha sem acoplamento de dados. +- **Suave:** B2 ✓ — `bahiaMunicipalityCodes` / `codeForMunicipality`. +- **Nenhuma dependência de A3/A4** — fonte IBGE ≠ TSE. - Reusa: `voterProfiles` + `NucleusIntelligenceDialog` + aba Eleitorado staff-only; padrão de artefato estático de B2. ## Não escopo @@ -77,13 +91,13 @@ Componentes: - Microdados IBGE / perfilamento de indivíduos (LGPD; fora de escopo absoluto). - Perfil por bairro/seção (sem série alinhada; E5 cobre Salvador por bairro no eixo eleitoral TSE, não demográfico). - Insights de voto A5 (conversão, classificação, alavancagem…) — planos `insight-*.md`. -- Camada demográfica no mapa Leaflet (B3) — eventual follow-up se o coroplético quiser densidade/renda. -- PNAD contínua, RAIS/CAGED, Estimativas anuais pós-2022 — fase futura se o Censo 2022 cobrir o ciclo. +- Camada demográfica no mapa Leaflet (B3) — eventual follow-up. +- PNAD contínua, RAIS/CAGED, Estimativas anuais pós-2022 — fase futura. - Abrir a aba Eleitorado para `lideranca` (mudança de access à parte). ## Referências -- `docs/roadmap.md` (Trilha A, item A8; Janela 3) +- `docs/roadmap.md` (Trilha A, item A8; Janela 2) - `src/collections/ElectoralNucleus.ts` — campo `voterProfiles` - `src/components/campaign/NucleusActiveTab.tsx` — `ElectorateContent` (staff-only) - `src/components/campaign/NucleusIntelligenceDialog.tsx` — editor manual @@ -92,6 +106,6 @@ Componentes: - `src/lib/bahiaTerritories.ts` — `citiesForTerritory` - `docs/plans/mapa-bahia-geometrias.md` — precedente de artefato estático + script CLI - `docs/plans/baseline-eleitoral-tse.md` — precedente de dado público agregado (TSE; fonte distinta) -- IBGE API Agregados: https://servicodados.ibge.gov.br/api/v3/agregados +- `docs/plans/escala-dry-pos-a8.md` — fill-in A8+ (lazy demographics, cobertura parcial, size budget) - IBGE SIDRA: https://apisidra.ibge.gov.br/ - AGENTS.md — naming EN/pt-BR; Campaign auth; sem PII; padrão estático B2 diff --git a/docs/roadmap.md b/docs/roadmap.md index fcec9324..bb461434 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap — Teqo -Atualizado em: 2026-07-19 (A5 mobilização ✓ em `main`; A5 aberto: só inteligência competitiva) +Atualizado em: 2026-07-20 (A8+ fill-in pós-`/simplify`; A8 em implementação) Registro canônico dos **próximos** planos e débitos. Histórico de entregas: resumo abaixo + planos em [`docs/plans/`](plans/) + notebook [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc). @@ -49,6 +49,7 @@ Engenharia de núcleos / C2 / C3 / baseline / PWA / Trilha E parcial já em `mai - **A** — A6 dobradinha 2026 automática quando o TSE publicar candidaturas · gatilho externo: pós-15/08 · [plano](plans/insight-dobradinha-2026.md) - **A** — A7 F2–F5: `cityCode` TSE, Alert/Progress DRY, fetch 2022 único lista+mapa, batch flip na lista · gatilho F4: pós-B3 ✓; F5: pós-A5 · [plano](plans/escala-dry-pos-a4.md) - **A** — A8 perfil médio do eleitorado (IBGE) na aba Eleitorado sem sobrescrever manuais · [plano](plans/perfil-eleitorado-ibge.md) +- **A** — A8+ escala pós-A8: lazy demographics, cobertura parcial, size budget · gatilho: merge A8 · [plano](plans/escala-dry-pos-a8.md) - **B** — B4 camada de zonas TSE no mapa coroplético · [mapa](plans/mapa-bahia-geometrias.md) - **B** — B5 F2–F3: cache CLI compartilhado + factory mun/TI · [plano](plans/escala-dry-pos-b2.md) - **B** — B6 `BahiaMap` setStyle incremental ao trocar métrica · gatilho: troca frequente no overview · [plano](plans/escala-dry-pos-b3.md) @@ -96,6 +97,7 @@ flowchart TD A7["A7 F2–F5 escala pós-A4"] A6["A6 Dobradinha 2026"] A8["A8 Perfis IBGE"] + A8DRY["A8+ escala pós-A8"] end subgraph TrilhaB["Trilha B"] @@ -131,6 +133,7 @@ flowchart TD A4 --> A5open A5done -.mesmo loader.-> A7 A4 --> A6 + A8 -.após merge.-> A8DRY TSE2026["TSE candidaturas 2026
(após 15/08)"] --> A6 B3 --> B6 B3 --> B4 @@ -188,6 +191,7 @@ flowchart TD ### Fill-ins abertos - **O0+** — revalidate globals, módulo de chaves Consent, testes SQL, DRY Lexical/layout · [plano](plans/escala-dry-pos-onda0.md) +- **A8+** — lazy `bahiaMunicipalityDemographics`, cobertura parcial explícita, size budget int · gatilho: A8 ✓ · [plano](plans/escala-dry-pos-a8.md) - **VR+** — refresh bfcache/storage do painel recentes + DRY de lista · gatilho F1: painel stale · [plano](plans/escala-dry-pos-visitados-recentemente.md) - **RS+** — auth read leve + shells/campos de senha compartilhados · gatilho F1: `/campanha` lento sob carga · [plano](plans/escala-dry-pos-reset-senha-perfil.md) - **FD+** — shell size / heading / strips Field Desk · gatilho strips: 3ª strip no home ou Fase 3 VoteGoals · [plano](plans/escala-dry-pos-field-desk.md) @@ -207,7 +211,7 @@ Plano histórico: [mapa-projecao-municipios.md](plans/mapa-projecao-municipios.m **Não cortáveis:** Onda 0 (jurídico/Consent), C2 dados reais, C3 agenda (já ✓), A4 baseline (já ✓), E1+E3 paridade mínima com a planilha (já ✓) — risco legal, base nominal, operação e instrumento de alocação. -**Cortes seguros** (se o prazo apertar): E5; B4; B6; B5 F2–F3; A8; E7 F1/F3/F4; E6 F2–F4 (preferir F1 se lista lenta); A7 F2–F5 (F4 vale mais com mapa na lista; F5 se filtros ≤10 núcleos); A5 competitiva; C4; D2 push (manter sino); C10/C11 se base/agenda pequenas; O0+/VR+/RS+/FD+ fases cosméticas; FD2 Fase 5 motion; fill-ins. +**Cortes seguros** (se o prazo apertar): E5; B4; B6; B5 F2–F3; A8; **A8+**; E7 F1/F3/F4; E6 F2–F4 (preferir F1 se lista lenta); A7 F2–F5 (F4 vale mais com mapa na lista; F5 se filtros ≤10 núcleos); A5 competitiva; C4; D2 push (manter sino); C10/C11 se base/agenda pequenas; O0+/VR+/RS+/FD+ fases cosméticas; FD2 Fase 5 motion; fill-ins. ## Bloqueadores atuais diff --git a/package.json b/package.json index 93ced912..c7dd384e 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "db:seed:posts": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-posts.mjs", "db:seed:tse": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-tse-results.mjs", "db:seed:consent": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-consent.mjs", + "build:demographics": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-demographics.mjs", "build:geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-geometries.mjs", "build:tse-city-codes": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-tse-city-codes.mjs", "dev": "node scripts/guard-dev-db.mjs && cross-env NODE_OPTIONS=--no-deprecation next dev", diff --git a/scripts/build-bahia-demographics.mjs b/scripts/build-bahia-demographics.mjs new file mode 100644 index 00000000..7133abb3 --- /dev/null +++ b/scripts/build-bahia-demographics.mjs @@ -0,0 +1,299 @@ +/** + * Builds static Bahia municipality demographics from IBGE SIDRA (Censo 2022). + * + * Provenance: + * - SIDRA table 9514 — População residente, por sexo, idade e forma de declaração da idade (2022, N6): + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022 + * - SIDRA table 9515 — Idade mediana (2022, N6): + * https://apisidra.ibge.gov.br/values/t/9515/n3/29/n6/all/v/10613/p/2022 + * + * Safety: does not touch any database — only downloads public SIDRA JSON and writes + * versioned artifacts under src/lib/ and tests/fixtures/. + * + * Usage: + * pnpm build:demographics + * DEMOGRAPHICS_CACHE_DIR=./data/demographics pnpm build:demographics + */ + +import { createHash } from 'node:crypto' +import { access, mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const { bahiaMunicipalityCodes } = await import('../src/lib/bahiaMunicipalityCodes.ts') +const { downloadToBuffer } = await import('../src/lib/electionResultsZip.ts') + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') + +const SOURCES = { + femalePopulation: { + key: 'sidra-9514-ba-municipios-mulheres', + url: 'https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/5/c287/100362', + }, + medianAge: { + key: 'sidra-9515-ba-municipios-idade-mediana', + url: 'https://apisidra.ibge.gov.br/values/t/9515/n3/29/n6/all/v/10613/p/2022', + }, +} + +/** SIDRA classification 287 (Idade) category ids grouped for the app bands. */ +const AGE_BAND_CATEGORY_IDS = { + '0-17': [93070, 93084, 93085, 6572, 6573, 6574], + '18-29': [6575, 6576, 6577, 6578, 6579, 6580, 6581, 93088], + '30-59': [93089, 93090, 93091, 93092, 93093, 93094], + '60+': [93095, 93096, 93097, 93098, 49108, 49109, 60040, 60041, 6653], +} + +const AGE_CATEGORY_TO_BAND = Object.fromEntries( + Object.entries(AGE_BAND_CATEGORY_IDS).flatMap(([band, ids]) => + ids.map((id) => [String(id), band]), + ), +) + +const die = (message) => { + console.error(`\n[build:demographics] ${message}\n`) + process.exit(1) +} + +const sha256 = (buffer) => createHash('sha256').update(buffer).digest('hex') + +const cacheDir = () => process.env.DEMOGRAPHICS_CACHE_DIR || join(ROOT, 'data', 'demographics') + +const ensureCachedJson = async ({ key, url }) => { + const dir = cacheDir() + await mkdir(dir, { recursive: true }) + const path = join(dir, `${key}.json`) + try { + await access(path) + console.log(`[build:demographics] cache hit ${path}`) + const buffer = await readFile(path) + return { url, hash: sha256(buffer), json: JSON.parse(buffer.toString('utf8')) } + } catch (error) { + if (error?.code !== 'ENOENT') throw error + } + console.log(`[build:demographics] downloading ${url}`) + const buffer = await downloadToBuffer(url) + const hash = sha256(buffer) + await writeFile(path, buffer) + console.log(`[build:demographics] saved ${path} (${buffer.length} bytes, sha256=${hash})`) + return { url, hash, json: JSON.parse(buffer.toString('utf8')) } +} + +const writeText = async (relativePath, body) => { + const path = join(ROOT, relativePath) + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, body) + console.log(`[build:demographics] wrote ${relativePath} (${Buffer.byteLength(body)} bytes)`) +} + +const parseSidraValue = (value) => { + if (value === '...' || value === '-' || value === 'X' || value == null || value === '') return 0 + const parsed = Number.parseInt(String(value).replace(/\./g, ''), 10) + return Number.isFinite(parsed) ? parsed : 0 +} + +const municipalityRows = (rows) => + rows.filter((row) => row?.NC === '6' && String(row?.D1C ?? '').startsWith('29')) + +const buildDemographicsByCode = ({ ageCategoryRows, femaleRows, medianRows }) => { + const byCode = new Map() + + for (const { categoryId, rows } of ageCategoryRows) { + const band = AGE_CATEGORY_TO_BAND[String(categoryId)] + if (!band) continue + + for (const row of municipalityRows(rows)) { + const code = String(row.D1C) + const current = byCode.get(code) ?? { + population: 0, + ageBands: { '0-17': 0, '18-29': 0, '30-59': 0, '60+': 0 }, + femalePopulation: 0, + medianAge: null, + } + current.ageBands[band] += parseSidraValue(row.V) + byCode.set(code, current) + } + } + + for (const row of municipalityRows(femaleRows)) { + const code = String(row.D1C) + const current = byCode.get(code) + if (!current) continue + current.femalePopulation = parseSidraValue(row.V) + } + + for (const row of municipalityRows(medianRows)) { + const code = String(row.D1C) + const current = byCode.get(code) + if (!current) continue + const median = Number.parseFloat(String(row.V).replace(',', '.')) + current.medianAge = Number.isFinite(median) ? median : null + } + + for (const [code, record] of byCode.entries()) { + record.population = Object.values(record.ageBands).reduce((sum, value) => sum + value, 0) + record.sexShareFemale = + record.population > 0 ? record.femalePopulation / record.population : 0 + delete record.femalePopulation + } + + return byCode +} + +const buildModuleSource = (demographicsByCode, provenance) => { + const entries = [...demographicsByCode.entries()].sort(([left], [right]) => left.localeCompare(right)) + const lines = entries + .map(([code, record]) => { + const bands = Object.entries(record.ageBands) + .map(([band, value]) => `${JSON.stringify(band)}: ${value}`) + .join(', ') + const median = + record.medianAge == null ? 'null' : JSON.stringify(record.medianAge) + const sexShare = record.sexShareFemale.toFixed(6) + return ` ${JSON.stringify(code)}: { population: ${record.population}, ageBands: { ${bands} }, sexShareFemale: ${sexShare}, medianAge: ${median} },` + }) + .join('\n') + + return `/** + * IBGE Censo 2022 municipality demographics for Bahia (codarea → indicators). + * + * Source (SIDRA table 9514 — população por idade, UF BA N6; accessed ${provenance.accessed}): + * ${provenance.ageCategoryUrls.join('\n * ')} + * Download SHA-256: ${provenance.ageCategorySha256.join(', ')} + * + * Source (SIDRA table 9514 — população feminina total, UF BA N6; accessed ${provenance.accessed}): + * ${provenance.femaleUrl} + * Download SHA-256: ${provenance.femaleSha256} + * + * Source (SIDRA table 9515 — idade mediana, UF BA N6; accessed ${provenance.accessed}): + * ${provenance.medianUrl} + * Download SHA-256: ${provenance.medianSha256} + * + * Age bands aggregate SIDRA classification 287 categories documented in scripts/build-bahia-demographics.mjs. + * Coverage is independently checked by tests/fixtures/bahia-municipality-demographics.official.json. + */ + +import { codeForMunicipality } from '@/lib/bahiaMunicipalityCodes' + +export type MunicipalityAgeBandKey = '0-17' | '18-29' | '30-59' | '60+' + +export type MunicipalityDemographics = { + population: number + ageBands: Record + sexShareFemale: number + medianAge: number | null +} + +export const bahiaMunicipalityDemographics: Readonly> = { +${lines} +} + +export const demographicsForCode = (codarea: string): MunicipalityDemographics | undefined => + bahiaMunicipalityDemographics[codarea] + +export const demographicsForMunicipalityName = ( + municipality: string, +): MunicipalityDemographics | undefined => { + const code = codeForMunicipality(municipality) + return code ? demographicsForCode(code) : undefined +} +` +} + +const buildFixture = (demographicsByCode) => { + const assignments = [...demographicsByCode.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([code, record]) => ({ + code, + population: record.population, + ageBands: record.ageBands, + sexShareFemale: Number(record.sexShareFemale.toFixed(6)), + medianAge: record.medianAge, + })) + + const evidenceRows = assignments + .map( + ({ code, population, ageBands, sexShareFemale, medianAge }) => + `D\t${code}\t${population}\t${ageBands['0-17']}\t${ageBands['18-29']}\t${ageBands['30-59']}\t${ageBands['60+']}\t${sexShareFemale}\t${medianAge ?? ''}\n`, + ) + .join('') + + return { + municipalityCount: assignments.length, + assignments, + evidenceSha256: sha256(Buffer.from(evidenceRows, 'utf8')), + } +} + +const main = async () => { + const accessed = new Date().toISOString().slice(0, 10) + const uniqueCategoryIds = [ + ...new Set(Object.values(AGE_BAND_CATEGORY_IDS).flatMap((ids) => ids)), + ].sort((left, right) => left - right) + + const ageCategoryDownloads = [] + for (const categoryId of uniqueCategoryIds) { + const download = await ensureCachedJson({ + key: `sidra-9514-ba-idade-${categoryId}`, + url: `https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/${categoryId}`, + }) + ageCategoryDownloads.push({ categoryId, ...download }) + } + + const female = await ensureCachedJson(SOURCES.femalePopulation) + const median = await ensureCachedJson(SOURCES.medianAge) + + const demographicsByCode = buildDemographicsByCode({ + ageCategoryRows: ageCategoryDownloads.map(({ categoryId, json }) => ({ + categoryId, + rows: json, + })), + femaleRows: female.json, + medianRows: median.json, + }) + + const expectedCodes = new Set(Object.values(bahiaMunicipalityCodes)) + if (demographicsByCode.size !== expectedCodes.size) { + die( + `expected ${expectedCodes.size} municipality rows, got ${demographicsByCode.size} from SIDRA`, + ) + } + + for (const code of expectedCodes) { + if (!demographicsByCode.has(code)) { + die(`missing demographics for codarea ${code}`) + } + const record = demographicsByCode.get(code) + if (record.population <= 0) { + die(`non-positive population for codarea ${code}`) + } + } + + const provenance = { + accessed, + ageCategoryUrls: ageCategoryDownloads.map(({ url }) => url), + ageCategorySha256: ageCategoryDownloads.map(({ hash }) => hash), + femaleUrl: female.url, + femaleSha256: female.hash, + medianUrl: median.url, + medianSha256: median.hash, + } + + await writeText( + 'src/lib/bahiaMunicipalityDemographics.ts', + buildModuleSource(demographicsByCode, provenance), + ) + await writeText( + 'tests/fixtures/bahia-municipality-demographics.official.json', + `${JSON.stringify(buildFixture(demographicsByCode), null, 2)}\n`, + ) + + console.log( + `[build:demographics] done — ${demographicsByCode.size} municipalities, total population ${[...demographicsByCode.values()].reduce((sum, row) => sum + row.population, 0)}`, + ) +} + +main().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/src/app/(campaign)/campanha/(app)/nucleos/[slug]/nucleusIbgeVoterProfileFormActions.ts b/src/app/(campaign)/campanha/(app)/nucleos/[slug]/nucleusIbgeVoterProfileFormActions.ts new file mode 100644 index 00000000..5bcb2b40 --- /dev/null +++ b/src/app/(campaign)/campanha/(app)/nucleos/[slug]/nucleusIbgeVoterProfileFormActions.ts @@ -0,0 +1,95 @@ +'use server' + +import { revalidatePath } from 'next/cache' + +import { updateNucleus } from '@/app/(campaign)/campanha/actions/nucleus' +import { requiredRelationshipFormValue } from '@/lib/formData' +import { getCampaignActionContext } from '@/utilities/campaignActionContext' +import { + type CampaignFormActionState, + mapCampaignFormActionError, +} from '@/utilities/campaignFormActionError' +import { toNucleusElectionGeographyInput } from '@/utilities/nucleusElectionGeography' +import { + getNucleusIbgeVoterProfile, + IBGE_VOTER_PROFILE_LABEL, +} from '@/utilities/nucleusIbgeVoterProfile' +import { voterProfileNullsToUndefined } from '@/utilities/nucleusIntelligenceUi' + +export type AppendIbgeVoterProfileFormState = CampaignFormActionState + +const safeMessages = [ + 'Somente a coordenação pode editar a inteligência do núcleo.', + 'Não há perfil IBGE disponível para este território.', + 'Já existe um perfil manual com este rótulo.', +] as const + +const errorState = (error: unknown): AppendIbgeVoterProfileFormState => + mapCampaignFormActionError({ + error, + safeMessages, + genericMessage: 'Não foi possível adicionar o perfil IBGE. Tente novamente.', + }) + +export const appendIbgeVoterProfileFormAction = async ( + _state: AppendIbgeVoterProfileFormState, + formData: FormData, +): Promise => { + try { + const { payload, actor } = await getCampaignActionContext() + if (actor.role === 'lideranca') { + throw new Error('Somente a coordenação pode editar a inteligência do núcleo.') + } + + const nucleusId = requiredRelationshipFormValue(formData, 'nucleus') + const nucleus = await payload.findByID({ + collection: 'electoralNucleus', + id: nucleusId, + depth: 0, + user: actor, + overrideAccess: false, + select: { + slug: true, + cities: true, + regions: true, + voterProfiles: { + label: true, + ageRange: true, + incomeBand: true, + occupation: true, + localTraits: true, + notes: true, + }, + }, + }) + + const computed = getNucleusIbgeVoterProfile(toNucleusElectionGeographyInput(nucleus)) + + if (computed.status !== 'available') { + throw new Error('Não há perfil IBGE disponível para este território.') + } + + const existingProfiles = (nucleus.voterProfiles ?? []).map(voterProfileNullsToUndefined) + if (existingProfiles.some((profile) => profile.label === IBGE_VOTER_PROFILE_LABEL)) { + throw new Error('Já existe um perfil manual com este rótulo.') + } + + const updated = await updateNucleus({ + id: nucleusId, + voterProfiles: [ + ...existingProfiles, + { + label: computed.profile.label, + ageRange: computed.profile.ageRange, + localTraits: computed.profile.localTraits, + notes: computed.profile.notes, + }, + ], + }) + + revalidatePath(`/campanha/nucleos/${updated.slug}`) + return { status: 'success', message: 'Perfil IBGE adicionado aos perfis manuais.' } + } catch (error) { + return errorState(error) + } +} diff --git a/src/components/campaign/NucleusActiveTab.tsx b/src/components/campaign/NucleusActiveTab.tsx index 9aca3b0f..a4ae64bf 100644 --- a/src/components/campaign/NucleusActiveTab.tsx +++ b/src/components/campaign/NucleusActiveTab.tsx @@ -41,7 +41,6 @@ import type { NucleusDetailViewModel, NucleusElectoralBaselineViewModel, NucleusTabsViewModel, - StaffNucleusTabsViewModel, } from '@/utilities/nucleusViewModels' import { buildWhatsAppUrl } from '@/utilities/phone' import { buildNucleusDetailMapHighlight } from '@/utilities/nucleusChoropleth' @@ -345,35 +344,6 @@ const TerritoryContent = ({ nucleus }: { nucleus: NucleusTabsViewModel }) => ( ) -const ElectorateContent = ({ nucleus }: { nucleus: StaffNucleusTabsViewModel }) => - nucleus.voterProfiles.length ? ( -
- {nucleus.voterProfiles.map((profile) => ( - - - {profile.label} - - - {profile.ageRange ?

Faixa etária: {profile.ageRange}

: null} - {profile.incomeBand ?

Renda: {profile.incomeBand}

: null} - {profile.occupation ?

Ocupação: {profile.occupation}

: null} - {profile.localTraits ?

{profile.localTraits}

: null} - {profile.notes ?

{profile.notes}

: null} -
-
- ))} -
- ) : ( - - - Nenhum perfil do eleitorado cadastrado - - Perfis e inteligência eleitoral serão trabalhados na próxima etapa. - - - - ) - export const NucleusActiveTabLoading = () => (
@@ -418,7 +388,14 @@ export const NucleusActiveTab = async ({ if (data.tab === 'territory') return if (data.tab === 'electorate') { if (view.tabs.kind !== 'staff') redirect(`/campanha/nucleos/${view.slug}?tab=overview`) - return + const { NucleusElectorateTab } = await import('@/components/campaign/NucleusElectorateTab') + return ( + + ) } if (data.tab === 'leaderships') { diff --git a/src/components/campaign/NucleusElectorateTab.tsx b/src/components/campaign/NucleusElectorateTab.tsx new file mode 100644 index 00000000..23a8ba9b --- /dev/null +++ b/src/components/campaign/NucleusElectorateTab.tsx @@ -0,0 +1,69 @@ +import { NucleusIbgeVoterProfile } from '@/components/campaign/NucleusIbgeVoterProfile' +import { NucleusManualVoterProfileCard } from '@/components/campaign/NucleusVoterProfileCard' +import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/Empty' +import { + getNucleusIbgeVoterProfile, + IBGE_VOTER_PROFILE_LABEL, +} from '@/utilities/nucleusIbgeVoterProfile' +import type { StaffNucleusTabsViewModel } from '@/utilities/nucleusViewModels' + +type NucleusElectorateTabProps = { + canEditIntelligence: boolean + nucleus: StaffNucleusTabsViewModel + nucleusId: number +} + +export const NucleusElectorateTab = ({ + canEditIntelligence, + nucleus, + nucleusId, +}: NucleusElectorateTabProps) => { + const computed = getNucleusIbgeVoterProfile({ + cities: nucleus.cities, + regions: nucleus.regions, + }) + const hasManualProfiles = nucleus.voterProfiles.length > 0 + const hasIbgeProfile = computed.status === 'available' + const hasManualProfileWithSameLabel = nucleus.voterProfiles.some( + (profile) => profile.label === IBGE_VOTER_PROFILE_LABEL, + ) + + if (!hasIbgeProfile && !hasManualProfiles) { + return ( + + + Nenhum perfil do eleitorado disponível + + Cadastre o território do núcleo para ver o perfil médio do IBGE ou adicione perfis + manuais pela inteligência do núcleo. + + + + ) + } + + return ( +
+ {hasIbgeProfile ? ( + + ) : null} + {hasManualProfiles ? ( +
+ {nucleus.voterProfiles.map((profile) => ( + + ))} +
+ ) : ( +

+ Nenhum perfil manual cadastrado. Use o botão acima para copiar o perfil IBGE ou edite pela + inteligência do núcleo. +

+ )} +
+ ) +} diff --git a/src/components/campaign/NucleusIbgeVoterProfile.tsx b/src/components/campaign/NucleusIbgeVoterProfile.tsx new file mode 100644 index 00000000..367b27cc --- /dev/null +++ b/src/components/campaign/NucleusIbgeVoterProfile.tsx @@ -0,0 +1,88 @@ +'use client' + +import { useActionState, useEffect } from 'react' +import { useRouter } from 'next/navigation' +import { toast } from 'sonner' + +import { + appendIbgeVoterProfileFormAction, + type AppendIbgeVoterProfileFormState, +} from '@/app/(campaign)/campanha/(app)/nucleos/[slug]/nucleusIbgeVoterProfileFormActions' +import { NucleusVoterProfileCardBody } from '@/components/campaign/NucleusVoterProfileCard' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert' +import { Badge } from '@/components/ui/Badge' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import type { ComputedVoterProfileViewModel } from '@/utilities/nucleusIbgeVoterProfile' + +type NucleusIbgeVoterProfileProps = { + canCopyToManual: boolean + hasManualProfileWithSameLabel: boolean + nucleusId: number + profile: ComputedVoterProfileViewModel +} + +const FormFeedback = ({ state }: { state: AppendIbgeVoterProfileFormState }) => { + if (!state.message || state.status === 'success') return null + + return ( + + Não foi possível adicionar o perfil + {state.message} + + ) +} + +export const NucleusIbgeVoterProfile = ({ + canCopyToManual, + hasManualProfileWithSameLabel, + nucleusId, + profile, +}: NucleusIbgeVoterProfileProps) => { + const router = useRouter() + const [state, formAction, pending] = useActionState< + AppendIbgeVoterProfileFormState, + FormData + >(appendIbgeVoterProfileFormAction, {}) + const displayState = state.status === 'success' ? {} : state + + useEffect(() => { + if (state.status !== 'success' || !state.message) return + toast.success(state.message) + router.refresh() + }, [router, state.message, state.status]) + + return ( + + +
+ {profile.label} + Calculado (IBGE Censo 2022) +
+
+ +

{profile.localTraits}

+ +

{profile.notes}

+ {canCopyToManual ? ( +
+ + +
+ ) : null} + +
+
+ ) +} diff --git a/src/components/campaign/NucleusIntelligenceDialog.tsx b/src/components/campaign/NucleusIntelligenceDialog.tsx index f7793b3b..95eae586 100644 --- a/src/components/campaign/NucleusIntelligenceDialog.tsx +++ b/src/components/campaign/NucleusIntelligenceDialog.tsx @@ -33,6 +33,7 @@ import { Input } from '@/components/ui/input' import { Spinner } from '@/components/ui/Spinner' import { Textarea } from '@/components/ui/textarea' import { fieldError } from '@/utilities/campaignFormFields' +import { normalizedVoterProfilesForWrite } from '@/utilities/nucleusIntelligenceUi' import { nucleusDobradinhaNotesDescription, nucleusDobradinhaNotesLabel, @@ -72,18 +73,6 @@ const emptyProfile = (): EditableProfile => ({ const normalizedInsights = (values: string[]) => values.map((text) => ({ text: text.trim() })).filter(({ text }) => Boolean(text)) -const normalizedProfiles = (profiles: EditableProfile[]) => - profiles - .map((profile) => ({ - label: profile.label.trim(), - ageRange: profile.ageRange?.trim() || undefined, - incomeBand: profile.incomeBand?.trim() || undefined, - occupation: profile.occupation?.trim() || undefined, - localTraits: profile.localTraits?.trim() || undefined, - notes: profile.notes?.trim() || undefined, - })) - .filter(({ label }) => Boolean(label)) - const FormFeedback = ({ state }: { state: NucleusIntelligenceFormState }) => { if (!state.message || state.status === 'success') return null @@ -166,7 +155,7 @@ export const NucleusIntelligenceDialog = ({ diff --git a/src/components/campaign/NucleusVoterProfileCard.tsx b/src/components/campaign/NucleusVoterProfileCard.tsx new file mode 100644 index 00000000..a8825fe0 --- /dev/null +++ b/src/components/campaign/NucleusVoterProfileCard.tsx @@ -0,0 +1,42 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' + +export type NucleusVoterProfileCardFields = { + label: string + ageRange?: string | null + incomeBand?: string | null + occupation?: string | null + localTraits?: string | null + notes?: string | null +} + +export const NucleusVoterProfileCardBody = ({ + profile, +}: { + profile: Pick< + NucleusVoterProfileCardFields, + 'ageRange' | 'incomeBand' | 'occupation' | 'localTraits' | 'notes' + > +}) => ( + <> + {profile.ageRange ?

Faixa etária: {profile.ageRange}

: null} + {profile.incomeBand ?

Renda: {profile.incomeBand}

: null} + {profile.occupation ?

Ocupação: {profile.occupation}

: null} + {profile.localTraits ?

{profile.localTraits}

: null} + {profile.notes ?

{profile.notes}

: null} + +) + +export const NucleusManualVoterProfileCard = ({ + profile, +}: { + profile: NucleusVoterProfileCardFields +}) => ( + + + {profile.label} + + + + + +) diff --git a/src/lib/bahiaMunicipalityDemographics.ts b/src/lib/bahiaMunicipalityDemographics.ts new file mode 100644 index 00000000..b160cc24 --- /dev/null +++ b/src/lib/bahiaMunicipalityDemographics.ts @@ -0,0 +1,487 @@ +/** + * IBGE Censo 2022 municipality demographics for Bahia (codarea → indicators). + * + * Source (SIDRA table 9514 — população por idade, UF BA N6; accessed 2026-07-20): + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/6572 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/6573 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/6574 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/6575 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/6576 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/6577 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/6578 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/6579 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/6580 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/6581 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/6653 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/49108 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/49109 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/60040 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/60041 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/93070 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/93084 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/93085 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/93088 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/93089 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/93090 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/93091 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/93092 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/93093 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/93094 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/93095 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/93096 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/93097 + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/6794/c287/93098 + * Download SHA-256: f86bf59de0d8afb5d61e49c9d839846f185d12774fa802db160ca912bb7c7ee1, 1881dc8e9162aa54fde56dfa027d53b84e46991b2400899a8cd8deaae9aacf46, 9a2ecd1e0f11d998d7c1966f78cc97ed758509d9f33fb269f63b39edf60ed0f4, 50e4863e19ec51df03edf2f4957ea0627d995dd4ed76ce459ea4174850cf54b3, 4cce7d106470128fea1e34d9c1c00ab71eee3395930e873129f354c6854e156e, 89d0f4db58500307f569d031b629e7566a360f8372345cb531c023d0f292f0b2, aa87fb15cc10e363847a73e2d472b0e119618adf501fcc4c973b7b330b8404dd, 470e5d2469a23c9c8555ea1d12b2a6f63d74229ad6ea00e9abf3140cd4816519, cb249824f40eb32167dd3518468be9f55146be60192ea66819102d18d1321e7d, 53fa99084b8c11e8afa39ffb1d91f9d6fc6b8fab853b0a04d9b9e8f33eff96c0, 4019a6c9b7be17b3e2a534f58f7522356c67466a9ff10abdd9bf77df81ef4ae4, 0f690efc75538ca79f15527525abcc5dcad89986a937320e1d5b1c25da0b1a2e, c504ecf6fa661d81f42094d0c92dc92b014af87f67265c3063d20f21350b494c, 3ea94b58779460035f8ec4cf34532eb96f9b8af136889c1a8ad840572ec88c4a, cac9ce52a4a175823fceccb35fc96c2bc36ff6aec1ce94af884523cee81247b7, 1012e2f02dd54c33aa1a3611d748e7cd889982e9124090e65591f4975c7b6fed, b6f3d4831499be4b7ec77b9f49fcf02da1a6357d7c96676c2349c45a7fb1665e, 513508fa7f32cce520f3b245c5e3ab339c5418403b09b1bfab930818617b8db7, bb1034d2e52ae3dc0f30c5ec70b49a002633c18b7bc8b642ff9246f00493146e, c430366cc2c66cacd848103590071d5fe22e564c46d5c43534d5baadb12927ce, 5831f5d92a82bd4c22c3d7a188a400a59d2835fa63331e7a31a6e0411c6a48c4, d7339f8216e85bb68911d039096336756190f6149434a73ce9e3f500c36c641f, 72dbaddff14e5d400e770fb92c84e32798b2e83759e51c05ffdeefadb591a60d, f113609923f9a0020c23cb57c576e280a4435a4b45d4a5a83c502db89e7ecabd, 7c6527c1cdede87a2fa7a34c893d0e275c692138ec1bfdca2fa0e17a131ec058, 29d515dedd5a339086eb94131efb2027c4e63f127af1e26ccb09f1710989093e, f9986ea56489a6ed4a335edbe3450ebb19c2db619f36a40a62bfeb3e19f9388d, 89777d7ba67ebed6039bc8745b759a776783d4448c1ab1f3a1df84a56ee01e59, c6d239a370853f014afd3cf9ddacb6ae2c3268eec2c36c4b509eaf046ccbb724 + * + * Source (SIDRA table 9514 — população feminina total, UF BA N6; accessed 2026-07-20): + * https://apisidra.ibge.gov.br/values/t/9514/n3/29/n6/all/v/93/p/2022/c2/5/c287/100362 + * Download SHA-256: 818eb751d25f88345de96772f25f77022c16cc849da43ececd528566cb85ff6c + * + * Source (SIDRA table 9515 — idade mediana, UF BA N6; accessed 2026-07-20): + * https://apisidra.ibge.gov.br/values/t/9515/n3/29/n6/all/v/10613/p/2022 + * Download SHA-256: 6eac88bf0963ff529addf390d9cf36fdad70f8d923923cf925251a500583d241 + * + * Age bands aggregate SIDRA classification 287 categories documented in scripts/build-bahia-demographics.mjs. + * Coverage is independently checked by tests/fixtures/bahia-municipality-demographics.official.json. + */ + +import { codeForMunicipality } from '@/lib/bahiaMunicipalityCodes' + +export type MunicipalityAgeBandKey = '0-17' | '18-29' | '30-59' | '60+' + +export type MunicipalityDemographics = { + population: number + ageBands: Record + sexShareFemale: number + medianAge: number | null +} + +export const bahiaMunicipalityDemographics: Readonly> = { + "2900108": { population: 7301, ageBands: { "0-17": 1356, "18-29": 967, "30-59": 3015, "60+": 1963 }, sexShareFemale: 0.511574, medianAge: 43 }, + "2900207": { population: 17639, ageBands: { "0-17": 5376, "18-29": 3442, "30-59": 6579, "60+": 2242 }, sexShareFemale: 0.506605, medianAge: 30 }, + "2900306": { population: 13795, ageBands: { "0-17": 3667, "18-29": 2485, "30-59": 5356, "60+": 2287 }, sexShareFemale: 0.511997, medianAge: 34 }, + "2900355": { population: 14201, ageBands: { "0-17": 3316, "18-29": 2711, "30-59": 5877, "60+": 2297 }, sexShareFemale: 0.502218, medianAge: 34 }, + "2900405": { population: 14497, ageBands: { "0-17": 3485, "18-29": 2511, "30-59": 5845, "60+": 2656 }, sexShareFemale: 0.514313, medianAge: 36 }, + "2900504": { population: 10604, ageBands: { "0-17": 2243, "18-29": 1992, "30-59": 4376, "60+": 1993 }, sexShareFemale: 0.501603, medianAge: 36 }, + "2900603": { population: 4447, ageBands: { "0-17": 1076, "18-29": 726, "30-59": 1794, "60+": 851 }, sexShareFemale: 0.501462, medianAge: 37 }, + "2900702": { population: 151055, ageBands: { "0-17": 36332, "18-29": 26395, "30-59": 66436, "60+": 21892 }, sexShareFemale: 0.531727, medianAge: 35 }, + "2900801": { population: 24530, ageBands: { "0-17": 6863, "18-29": 4117, "30-59": 9751, "60+": 3799 }, sexShareFemale: 0.495516, medianAge: 33 }, + "2900900": { population: 5218, ageBands: { "0-17": 1198, "18-29": 754, "30-59": 2104, "60+": 1162 }, sexShareFemale: 0.510349, medianAge: 39 }, + "2901007": { population: 36521, ageBands: { "0-17": 8711, "18-29": 7045, "30-59": 14851, "60+": 5914 }, sexShareFemale: 0.518880, medianAge: 34 }, + "2901106": { population: 24138, ageBands: { "0-17": 5818, "18-29": 4102, "30-59": 10226, "60+": 3992 }, sexShareFemale: 0.533640, medianAge: 36 }, + "2901155": { population: 15137, ageBands: { "0-17": 4383, "18-29": 2683, "30-59": 5767, "60+": 2304 }, sexShareFemale: 0.495937, medianAge: 32 }, + "2901205": { population: 25438, ageBands: { "0-17": 5762, "18-29": 4079, "30-59": 10162, "60+": 5435 }, sexShareFemale: 0.496580, medianAge: 38 }, + "2901304": { population: 13080, ageBands: { "0-17": 3491, "18-29": 2448, "30-59": 5117, "60+": 2024 }, sexShareFemale: 0.498089, medianAge: 33 }, + "2901353": { population: 15012, ageBands: { "0-17": 3542, "18-29": 2696, "30-59": 6354, "60+": 2420 }, sexShareFemale: 0.502598, medianAge: 35 }, + "2901403": { population: 13732, ageBands: { "0-17": 3625, "18-29": 2371, "30-59": 5438, "60+": 2298 }, sexShareFemale: 0.485071, medianAge: 34 }, + "2901502": { population: 11031, ageBands: { "0-17": 2503, "18-29": 2002, "30-59": 4679, "60+": 1847 }, sexShareFemale: 0.510652, medianAge: 36 }, + "2901601": { population: 14206, ageBands: { "0-17": 2896, "18-29": 2648, "30-59": 6059, "60+": 2603 }, sexShareFemale: 0.498381, medianAge: 36 }, + "2901700": { population: 11146, ageBands: { "0-17": 3011, "18-29": 2091, "30-59": 4351, "60+": 1693 }, sexShareFemale: 0.514265, medianAge: 32 }, + "2901809": { population: 10862, ageBands: { "0-17": 2779, "18-29": 1916, "30-59": 4451, "60+": 1716 }, sexShareFemale: 0.499632, medianAge: 34 }, + "2901908": { population: 15922, ageBands: { "0-17": 4037, "18-29": 2834, "30-59": 6279, "60+": 2772 }, sexShareFemale: 0.496608, medianAge: 35 }, + "2901957": { population: 6913, ageBands: { "0-17": 1775, "18-29": 1195, "30-59": 2661, "60+": 1282 }, sexShareFemale: 0.496167, medianAge: 35 }, + "2902005": { population: 13936, ageBands: { "0-17": 3211, "18-29": 2304, "30-59": 5656, "60+": 2765 }, sexShareFemale: 0.489165, medianAge: 37 }, + "2902054": { population: 11557, ageBands: { "0-17": 3203, "18-29": 2310, "30-59": 4642, "60+": 1402 }, sexShareFemale: 0.516916, medianAge: 31 }, + "2902104": { population: 48294, ageBands: { "0-17": 13010, "18-29": 9431, "30-59": 18633, "60+": 7220 }, sexShareFemale: 0.501097, medianAge: 32 }, + "2902203": { population: 9833, ageBands: { "0-17": 2429, "18-29": 1786, "30-59": 4131, "60+": 1487 }, sexShareFemale: 0.515916, medianAge: 35 }, + "2902252": { population: 10191, ageBands: { "0-17": 2737, "18-29": 1869, "30-59": 3985, "60+": 1600 }, sexShareFemale: 0.472181, medianAge: 33 }, + "2902302": { population: 8677, ageBands: { "0-17": 2261, "18-29": 1602, "30-59": 3639, "60+": 1175 }, sexShareFemale: 0.495448, medianAge: 33 }, + "2902401": { population: 11179, ageBands: { "0-17": 3258, "18-29": 1881, "30-59": 4287, "60+": 1753 }, sexShareFemale: 0.507738, medianAge: 33 }, + "2902500": { population: 13614, ageBands: { "0-17": 3668, "18-29": 2449, "30-59": 5297, "60+": 2200 }, sexShareFemale: 0.485897, medianAge: 33 }, + "2902609": { population: 18220, ageBands: { "0-17": 4500, "18-29": 3192, "30-59": 7054, "60+": 3474 }, sexShareFemale: 0.503238, medianAge: 36 }, + "2902658": { population: 11958, ageBands: { "0-17": 3360, "18-29": 2096, "30-59": 4583, "60+": 1919 }, sexShareFemale: 0.496153, medianAge: 33 }, + "2902708": { population: 51092, ageBands: { "0-17": 16288, "18-29": 9907, "30-59": 18520, "60+": 6377 }, sexShareFemale: 0.491290, medianAge: 29 }, + "2902807": { population: 26026, ageBands: { "0-17": 6778, "18-29": 5221, "30-59": 10605, "60+": 3422 }, sexShareFemale: 0.495504, medianAge: 32 }, + "2902906": { population: 36539, ageBands: { "0-17": 10124, "18-29": 6696, "30-59": 14500, "60+": 5219 }, sexShareFemale: 0.507540, medianAge: 32 }, + "2903003": { population: 13836, ageBands: { "0-17": 3320, "18-29": 2314, "30-59": 5383, "60+": 2819 }, sexShareFemale: 0.495302, medianAge: 37 }, + "2903102": { population: 5775, ageBands: { "0-17": 1469, "18-29": 919, "30-59": 2377, "60+": 1010 }, sexShareFemale: 0.477576, medianAge: 36 }, + "2903201": { population: 159734, ageBands: { "0-17": 43478, "18-29": 32658, "30-59": 67612, "60+": 15986 }, sexShareFemale: 0.510943, medianAge: 31 }, + "2903235": { population: 13453, ageBands: { "0-17": 3375, "18-29": 2431, "30-59": 5434, "60+": 2213 }, sexShareFemale: 0.494908, medianAge: 35 }, + "2903276": { population: 15203, ageBands: { "0-17": 4005, "18-29": 2742, "30-59": 6371, "60+": 2085 }, sexShareFemale: 0.505558, medianAge: 33 }, + "2903300": { population: 5833, ageBands: { "0-17": 1354, "18-29": 955, "30-59": 2420, "60+": 1104 }, sexShareFemale: 0.499914, medianAge: 37 }, + "2903409": { population: 20121, ageBands: { "0-17": 5662, "18-29": 3459, "30-59": 7881, "60+": 3119 }, sexShareFemale: 0.496446, medianAge: 33 }, + "2903508": { population: 18412, ageBands: { "0-17": 4472, "18-29": 3144, "30-59": 7193, "60+": 3603 }, sexShareFemale: 0.501195, medianAge: 36 }, + "2903607": { population: 15146, ageBands: { "0-17": 3852, "18-29": 2663, "30-59": 6108, "60+": 2523 }, sexShareFemale: 0.497425, medianAge: 34 }, + "2903706": { population: 13690, ageBands: { "0-17": 3430, "18-29": 2343, "30-59": 5196, "60+": 2721 }, sexShareFemale: 0.501899, medianAge: 35 }, + "2903805": { population: 16873, ageBands: { "0-17": 4650, "18-29": 2774, "30-59": 6506, "60+": 2943 }, sexShareFemale: 0.492977, medianAge: 34 }, + "2903904": { population: 65550, ageBands: { "0-17": 18854, "18-29": 12667, "30-59": 25605, "60+": 8424 }, sexShareFemale: 0.509260, medianAge: 31 }, + "2903953": { population: 9730, ageBands: { "0-17": 2415, "18-29": 1810, "30-59": 3754, "60+": 1751 }, sexShareFemale: 0.483864, medianAge: 34 }, + "2904001": { population: 13622, ageBands: { "0-17": 3496, "18-29": 2319, "30-59": 5329, "60+": 2478 }, sexShareFemale: 0.510277, medianAge: 35 }, + "2904050": { population: 15844, ageBands: { "0-17": 4686, "18-29": 3410, "30-59": 6066, "60+": 1682 }, sexShareFemale: 0.500063, medianAge: 29 }, + "2904100": { population: 19322, ageBands: { "0-17": 4699, "18-29": 3340, "30-59": 7484, "60+": 3799 }, sexShareFemale: 0.508074, medianAge: 36 }, + "2904209": { population: 11024, ageBands: { "0-17": 2518, "18-29": 2083, "30-59": 4374, "60+": 2049 }, sexShareFemale: 0.497823, medianAge: 35 }, + "2904308": { population: 12943, ageBands: { "0-17": 3268, "18-29": 2213, "30-59": 5124, "60+": 2338 }, sexShareFemale: 0.507997, medianAge: 35 }, + "2904407": { population: 9108, ageBands: { "0-17": 2222, "18-29": 1300, "30-59": 3758, "60+": 1828 }, sexShareFemale: 0.485617, medianAge: 38 }, + "2904506": { population: 11765, ageBands: { "0-17": 2906, "18-29": 1833, "30-59": 4489, "60+": 2537 }, sexShareFemale: 0.487123, medianAge: 37 }, + "2904605": { population: 70510, ageBands: { "0-17": 16094, "18-29": 12337, "30-59": 30638, "60+": 11441 }, sexShareFemale: 0.512523, medianAge: 36 }, + "2904704": { population: 14804, ageBands: { "0-17": 3609, "18-29": 2298, "30-59": 5830, "60+": 3067 }, sexShareFemale: 0.512497, medianAge: 37 }, + "2904753": { population: 19589, ageBands: { "0-17": 5733, "18-29": 3806, "30-59": 7509, "60+": 2541 }, sexShareFemale: 0.479249, medianAge: 30 }, + "2904803": { population: 6205, ageBands: { "0-17": 1435, "18-29": 1001, "30-59": 2627, "60+": 1142 }, sexShareFemale: 0.486382, medianAge: 37 }, + "2904852": { population: 16559, ageBands: { "0-17": 4683, "18-29": 3292, "30-59": 6318, "60+": 2266 }, sexShareFemale: 0.517362, medianAge: 31 }, + "2904902": { population: 29250, ageBands: { "0-17": 7030, "18-29": 5402, "30-59": 12121, "60+": 4697 }, sexShareFemale: 0.521846, medianAge: 35 }, + "2905008": { population: 22462, ageBands: { "0-17": 5044, "18-29": 3987, "30-59": 9293, "60+": 4138 }, sexShareFemale: 0.506411, medianAge: 36 }, + "2905107": { population: 10384, ageBands: { "0-17": 2806, "18-29": 1890, "30-59": 3948, "60+": 1740 }, sexShareFemale: 0.503948, medianAge: 33 }, + "2905156": { population: 11266, ageBands: { "0-17": 2663, "18-29": 1921, "30-59": 4556, "60+": 2126 }, sexShareFemale: 0.488638, medianAge: 36 }, + "2905206": { population: 52012, ageBands: { "0-17": 12293, "18-29": 9487, "30-59": 21605, "60+": 8627 }, sexShareFemale: 0.508979, medianAge: 34 }, + "2905305": { population: 17466, ageBands: { "0-17": 4890, "18-29": 3328, "30-59": 6865, "60+": 2383 }, sexShareFemale: 0.494733, medianAge: 32 }, + "2905404": { population: 17761, ageBands: { "0-17": 4887, "18-29": 3701, "30-59": 7453, "60+": 1720 }, sexShareFemale: 0.510669, medianAge: 30 }, + "2905503": { population: 13080, ageBands: { "0-17": 3634, "18-29": 2411, "30-59": 5006, "60+": 2029 }, sexShareFemale: 0.506040, medianAge: 32 }, + "2905602": { population: 22579, ageBands: { "0-17": 6159, "18-29": 3892, "30-59": 8514, "60+": 4014 }, sexShareFemale: 0.513265, medianAge: 34 }, + "2905701": { population: 300372, ageBands: { "0-17": 77870, "18-29": 53220, "30-59": 136559, "60+": 32723 }, sexShareFemale: 0.519696, medianAge: 34 }, + "2905800": { population: 30469, ageBands: { "0-17": 9565, "18-29": 5474, "30-59": 11196, "60+": 4234 }, sexShareFemale: 0.510289, medianAge: 30 }, + "2905909": { population: 30671, ageBands: { "0-17": 9034, "18-29": 5266, "30-59": 11674, "60+": 4697 }, sexShareFemale: 0.493952, medianAge: 32 }, + "2906006": { population: 71377, ageBands: { "0-17": 19162, "18-29": 13848, "30-59": 28262, "60+": 10105 }, sexShareFemale: 0.502711, medianAge: 32 }, + "2906105": { population: 10225, ageBands: { "0-17": 2785, "18-29": 1866, "30-59": 3882, "60+": 1692 }, sexShareFemale: 0.481174, medianAge: 33 }, + "2906204": { population: 24206, ageBands: { "0-17": 6536, "18-29": 4573, "30-59": 9467, "60+": 3630 }, sexShareFemale: 0.501983, medianAge: 33 }, + "2906303": { population: 32683, ageBands: { "0-17": 8439, "18-29": 5498, "30-59": 13139, "60+": 5607 }, sexShareFemale: 0.500382, medianAge: 35 }, + "2906402": { population: 7772, ageBands: { "0-17": 1763, "18-29": 1350, "30-59": 3112, "60+": 1547 }, sexShareFemale: 0.502316, medianAge: 38 }, + "2906501": { population: 72382, ageBands: { "0-17": 17722, "18-29": 12420, "30-59": 32220, "60+": 10020 }, sexShareFemale: 0.531334, medianAge: 35 }, + "2906600": { population: 13016, ageBands: { "0-17": 2868, "18-29": 2002, "30-59": 5390, "60+": 2756 }, sexShareFemale: 0.497849, medianAge: 38 }, + "2906709": { population: 25247, ageBands: { "0-17": 6386, "18-29": 4170, "30-59": 10177, "60+": 4514 }, sexShareFemale: 0.502515, medianAge: 36 }, + "2906808": { population: 37439, ageBands: { "0-17": 9773, "18-29": 7548, "30-59": 14553, "60+": 5565 }, sexShareFemale: 0.497182, medianAge: 32 }, + "2906824": { population: 16105, ageBands: { "0-17": 4539, "18-29": 2813, "30-59": 6332, "60+": 2421 }, sexShareFemale: 0.494753, medianAge: 33 }, + "2906857": { population: 10744, ageBands: { "0-17": 2284, "18-29": 1658, "30-59": 4455, "60+": 2347 }, sexShareFemale: 0.502699, medianAge: 39 }, + "2906873": { population: 33235, ageBands: { "0-17": 8822, "18-29": 6196, "30-59": 13457, "60+": 4760 }, sexShareFemale: 0.513886, medianAge: 33 }, + "2906899": { population: 9940, ageBands: { "0-17": 2229, "18-29": 1553, "30-59": 3837, "60+": 2321 }, sexShareFemale: 0.492656, medianAge: 39 }, + "2906907": { population: 20580, ageBands: { "0-17": 5638, "18-29": 3633, "30-59": 8060, "60+": 3249 }, sexShareFemale: 0.501555, medianAge: 33 }, + "2907004": { population: 8365, ageBands: { "0-17": 2457, "18-29": 1700, "30-59": 3231, "60+": 977 }, sexShareFemale: 0.516916, medianAge: 30 }, + "2907103": { population: 28869, ageBands: { "0-17": 8498, "18-29": 5315, "30-59": 11113, "60+": 3943 }, sexShareFemale: 0.489764, medianAge: 31 }, + "2907202": { population: 72086, ageBands: { "0-17": 20699, "18-29": 13940, "30-59": 27931, "60+": 9516 }, sexShareFemale: 0.488652, medianAge: 31 }, + "2907301": { population: 24712, ageBands: { "0-17": 5938, "18-29": 4382, "30-59": 10385, "60+": 4007 }, sexShareFemale: 0.526020, medianAge: 35 }, + "2907400": { population: 3434, ageBands: { "0-17": 941, "18-29": 491, "30-59": 1359, "60+": 643 }, sexShareFemale: 0.481072, medianAge: 36 }, + "2907509": { population: 48148, ageBands: { "0-17": 11182, "18-29": 7790, "30-59": 21652, "60+": 7524 }, sexShareFemale: 0.532836, medianAge: 37 }, + "2907558": { population: 8841, ageBands: { "0-17": 1939, "18-29": 1513, "30-59": 3584, "60+": 1805 }, sexShareFemale: 0.491460, medianAge: 37 }, + "2907608": { population: 16348, ageBands: { "0-17": 4218, "18-29": 2710, "30-59": 6514, "60+": 2906 }, sexShareFemale: 0.493883, medianAge: 35 }, + "2907707": { population: 10579, ageBands: { "0-17": 2990, "18-29": 2045, "30-59": 3957, "60+": 1587 }, sexShareFemale: 0.494092, medianAge: 31 }, + "2907806": { population: 30907, ageBands: { "0-17": 6712, "18-29": 5412, "30-59": 12931, "60+": 5852 }, sexShareFemale: 0.506973, medianAge: 37 }, + "2907905": { population: 17230, ageBands: { "0-17": 4762, "18-29": 3195, "30-59": 6861, "60+": 2412 }, sexShareFemale: 0.512362, medianAge: 32 }, + "2908002": { population: 17333, ageBands: { "0-17": 4061, "18-29": 2657, "30-59": 6743, "60+": 3872 }, sexShareFemale: 0.523337, medianAge: 38 }, + "2908101": { population: 19151, ageBands: { "0-17": 4884, "18-29": 3645, "30-59": 7375, "60+": 3247 }, sexShareFemale: 0.489269, medianAge: 33 }, + "2908200": { population: 20800, ageBands: { "0-17": 5174, "18-29": 3976, "30-59": 8683, "60+": 2967 }, sexShareFemale: 0.525817, medianAge: 34 }, + "2908309": { population: 15794, ageBands: { "0-17": 3335, "18-29": 2616, "30-59": 6916, "60+": 2927 }, sexShareFemale: 0.522730, medianAge: 38 }, + "2908408": { population: 67825, ageBands: { "0-17": 17610, "18-29": 12242, "30-59": 27937, "60+": 10036 }, sexShareFemale: 0.514943, medianAge: 34 }, + "2908507": { population: 35308, ageBands: { "0-17": 8455, "18-29": 6412, "30-59": 15506, "60+": 4935 }, sexShareFemale: 0.524584, medianAge: 35 }, + "2908606": { population: 23654, ageBands: { "0-17": 6892, "18-29": 4956, "30-59": 8755, "60+": 3051 }, sexShareFemale: 0.504777, medianAge: 29 }, + "2908705": { population: 17053, ageBands: { "0-17": 3817, "18-29": 2609, "30-59": 6976, "60+": 3651 }, sexShareFemale: 0.507242, medianAge: 39 }, + "2908804": { population: 4333, ageBands: { "0-17": 1005, "18-29": 729, "30-59": 1721, "60+": 878 }, sexShareFemale: 0.494115, medianAge: 36 }, + "2908903": { population: 26692, ageBands: { "0-17": 6380, "18-29": 5015, "30-59": 11202, "60+": 4095 }, sexShareFemale: 0.519069, medianAge: 34 }, + "2909000": { population: 7546, ageBands: { "0-17": 1618, "18-29": 1136, "30-59": 3129, "60+": 1663 }, sexShareFemale: 0.509674, medianAge: 40 }, + "2909109": { population: 13990, ageBands: { "0-17": 3355, "18-29": 2194, "30-59": 5698, "60+": 2743 }, sexShareFemale: 0.488277, medianAge: 37 }, + "2909208": { population: 17056, ageBands: { "0-17": 4364, "18-29": 3443, "30-59": 6774, "60+": 2475 }, sexShareFemale: 0.487394, medianAge: 32 }, + "2909307": { population: 32457, ageBands: { "0-17": 8374, "18-29": 5784, "30-59": 13168, "60+": 5131 }, sexShareFemale: 0.498629, medianAge: 34 }, + "2909406": { population: 13063, ageBands: { "0-17": 3557, "18-29": 2272, "30-59": 5106, "60+": 2128 }, sexShareFemale: 0.486948, medianAge: 33 }, + "2909505": { population: 4415, ageBands: { "0-17": 1085, "18-29": 671, "30-59": 1832, "60+": 827 }, sexShareFemale: 0.504190, medianAge: 37 }, + "2909604": { population: 19729, ageBands: { "0-17": 5124, "18-29": 3476, "30-59": 7891, "60+": 3238 }, sexShareFemale: 0.498657, medianAge: 35 }, + "2909703": { population: 13993, ageBands: { "0-17": 3520, "18-29": 2538, "30-59": 5667, "60+": 2268 }, sexShareFemale: 0.486815, medianAge: 34 }, + "2909802": { population: 60348, ageBands: { "0-17": 13723, "18-29": 10492, "30-59": 26851, "60+": 9282 }, sexShareFemale: 0.534135, medianAge: 36 }, + "2909901": { population: 34180, ageBands: { "0-17": 9974, "18-29": 6420, "30-59": 13254, "60+": 4532 }, sexShareFemale: 0.489585, medianAge: 31 }, + "2910008": { population: 10820, ageBands: { "0-17": 2976, "18-29": 1772, "30-59": 4142, "60+": 1930 }, sexShareFemale: 0.481701, medianAge: 34 }, + "2910057": { population: 71485, ageBands: { "0-17": 19227, "18-29": 12666, "30-59": 31098, "60+": 8494 }, sexShareFemale: 0.519326, medianAge: 33 }, + "2910107": { population: 11884, ageBands: { "0-17": 2585, "18-29": 2061, "30-59": 5170, "60+": 2068 }, sexShareFemale: 0.489650, medianAge: 36 }, + "2910206": { population: 4407, ageBands: { "0-17": 997, "18-29": 741, "30-59": 1912, "60+": 757 }, sexShareFemale: 0.515770, medianAge: 37 }, + "2910305": { population: 7808, ageBands: { "0-17": 1724, "18-29": 1132, "30-59": 3310, "60+": 1642 }, sexShareFemale: 0.504355, medianAge: 39 }, + "2910404": { population: 19107, ageBands: { "0-17": 5136, "18-29": 3309, "30-59": 7460, "60+": 3202 }, sexShareFemale: 0.483906, medianAge: 34 }, + "2910503": { population: 38098, ageBands: { "0-17": 10048, "18-29": 7282, "30-59": 15658, "60+": 5110 }, sexShareFemale: 0.519922, medianAge: 33 }, + "2910602": { population: 32554, ageBands: { "0-17": 9194, "18-29": 6571, "30-59": 12574, "60+": 4215 }, sexShareFemale: 0.513792, medianAge: 31 }, + "2910701": { population: 61456, ageBands: { "0-17": 15452, "18-29": 10987, "30-59": 24817, "60+": 10200 }, sexShareFemale: 0.501188, medianAge: 34 }, + "2910727": { population: 113710, ageBands: { "0-17": 30851, "18-29": 20849, "30-59": 47401, "60+": 14609 }, sexShareFemale: 0.515161, medianAge: 33 }, + "2910750": { population: 17895, ageBands: { "0-17": 3976, "18-29": 3285, "30-59": 7564, "60+": 3070 }, sexShareFemale: 0.499357, medianAge: 36 }, + "2910776": { population: 5631, ageBands: { "0-17": 1362, "18-29": 876, "30-59": 2345, "60+": 1048 }, sexShareFemale: 0.487658, medianAge: 37 }, + "2910800": { population: 616272, ageBands: { "0-17": 150536, "18-29": 116997, "30-59": 269294, "60+": 79445 }, sexShareFemale: 0.534266, medianAge: 34 }, + "2910859": { population: 17897, ageBands: { "0-17": 4622, "18-29": 3346, "30-59": 7229, "60+": 2700 }, sexShareFemale: 0.501034, medianAge: 33 }, + "2910909": { population: 4873, ageBands: { "0-17": 1229, "18-29": 819, "30-59": 1947, "60+": 878 }, sexShareFemale: 0.496204, medianAge: 35 }, + "2911006": { population: 11059, ageBands: { "0-17": 2515, "18-29": 1765, "30-59": 4588, "60+": 2191 }, sexShareFemale: 0.491184, medianAge: 37 }, + "2911105": { population: 25899, ageBands: { "0-17": 7730, "18-29": 5289, "30-59": 9757, "60+": 3123 }, sexShareFemale: 0.494421, medianAge: 29 }, + "2911204": { population: 32178, ageBands: { "0-17": 8512, "18-29": 5687, "30-59": 13469, "60+": 4510 }, sexShareFemale: 0.511095, medianAge: 34 }, + "2911253": { population: 4360, ageBands: { "0-17": 960, "18-29": 693, "30-59": 1774, "60+": 933 }, sexShareFemale: 0.486697, medianAge: 38 }, + "2911303": { population: 10884, ageBands: { "0-17": 2709, "18-29": 1947, "30-59": 4294, "60+": 1934 }, sexShareFemale: 0.486953, medianAge: 35 }, + "2911402": { population: 15524, ageBands: { "0-17": 4230, "18-29": 2576, "30-59": 6425, "60+": 2293 }, sexShareFemale: 0.509469, medianAge: 34 }, + "2911501": { population: 5555, ageBands: { "0-17": 1459, "18-29": 922, "30-59": 2150, "60+": 1024 }, sexShareFemale: 0.501350, medianAge: 35 }, + "2911600": { population: 20605, ageBands: { "0-17": 5149, "18-29": 3563, "30-59": 8686, "60+": 3207 }, sexShareFemale: 0.531230, medianAge: 35 }, + "2911659": { population: 8050, ageBands: { "0-17": 1677, "18-29": 1320, "30-59": 3200, "60+": 1853 }, sexShareFemale: 0.500870, medianAge: 40 }, + "2911709": { population: 87817, ageBands: { "0-17": 20825, "18-29": 15921, "30-59": 37560, "60+": 13511 }, sexShareFemale: 0.515117, medianAge: 35 }, + "2911808": { population: 19049, ageBands: { "0-17": 4875, "18-29": 2935, "30-59": 7625, "60+": 3614 }, sexShareFemale: 0.485747, medianAge: 36 }, + "2911857": { population: 12309, ageBands: { "0-17": 2676, "18-29": 2406, "30-59": 5075, "60+": 2152 }, sexShareFemale: 0.497928, medianAge: 36 }, + "2911907": { population: 24607, ageBands: { "0-17": 6448, "18-29": 4253, "30-59": 9625, "60+": 4281 }, sexShareFemale: 0.516032, medianAge: 35 }, + "2912004": { population: 10429, ageBands: { "0-17": 2049, "18-29": 1671, "30-59": 4426, "60+": 2283 }, sexShareFemale: 0.500431, medianAge: 40 }, + "2912103": { population: 21665, ageBands: { "0-17": 5065, "18-29": 3617, "30-59": 8526, "60+": 4457 }, sexShareFemale: 0.518024, medianAge: 37 }, + "2912202": { population: 20785, ageBands: { "0-17": 6169, "18-29": 4327, "30-59": 8277, "60+": 2012 }, sexShareFemale: 0.498773, medianAge: 29 }, + "2912301": { population: 13934, ageBands: { "0-17": 3303, "18-29": 2226, "30-59": 5687, "60+": 2718 }, sexShareFemale: 0.502655, medianAge: 37 }, + "2912400": { population: 16603, ageBands: { "0-17": 4196, "18-29": 2702, "30-59": 6818, "60+": 2887 }, sexShareFemale: 0.494429, medianAge: 36 }, + "2912509": { population: 13863, ageBands: { "0-17": 3118, "18-29": 2590, "30-59": 5543, "60+": 2612 }, sexShareFemale: 0.502489, medianAge: 36 }, + "2912608": { population: 3725, ageBands: { "0-17": 990, "18-29": 631, "30-59": 1464, "60+": 640 }, sexShareFemale: 0.488322, medianAge: 35 }, + "2912707": { population: 25344, ageBands: { "0-17": 7211, "18-29": 4981, "30-59": 9411, "60+": 3741 }, sexShareFemale: 0.483546, medianAge: 31 }, + "2912806": { population: 8896, ageBands: { "0-17": 2316, "18-29": 1518, "30-59": 3565, "60+": 1497 }, sexShareFemale: 0.497639, medianAge: 34 }, + "2912905": { population: 18792, ageBands: { "0-17": 4850, "18-29": 2874, "30-59": 7602, "60+": 3466 }, sexShareFemale: 0.513995, medianAge: 36 }, + "2913002": { population: 14637, ageBands: { "0-17": 3187, "18-29": 2525, "30-59": 5850, "60+": 3075 }, sexShareFemale: 0.501674, medianAge: 37 }, + "2913101": { population: 16969, ageBands: { "0-17": 4186, "18-29": 2790, "30-59": 6968, "60+": 3025 }, sexShareFemale: 0.497613, medianAge: 36 }, + "2913200": { population: 26309, ageBands: { "0-17": 7256, "18-29": 4823, "30-59": 10449, "60+": 3781 }, sexShareFemale: 0.510016, medianAge: 32 }, + "2913309": { population: 6190, ageBands: { "0-17": 1444, "18-29": 1079, "30-59": 2521, "60+": 1146 }, sexShareFemale: 0.513409, medianAge: 36 }, + "2913408": { population: 15527, ageBands: { "0-17": 3427, "18-29": 2795, "30-59": 6460, "60+": 2845 }, sexShareFemale: 0.507825, medianAge: 36 }, + "2913457": { population: 13151, ageBands: { "0-17": 3627, "18-29": 2525, "30-59": 5266, "60+": 1733 }, sexShareFemale: 0.485210, medianAge: 32 }, + "2913507": { population: 21491, ageBands: { "0-17": 5467, "18-29": 3426, "30-59": 8608, "60+": 3990 }, sexShareFemale: 0.493788, medianAge: 36 }, + "2913606": { population: 178649, ageBands: { "0-17": 42464, "18-29": 30693, "30-59": 76088, "60+": 29404 }, sexShareFemale: 0.521173, medianAge: 36 }, + "2913705": { population: 33790, ageBands: { "0-17": 8968, "18-29": 6430, "30-59": 13416, "60+": 4976 }, sexShareFemale: 0.511246, medianAge: 33 }, + "2913804": { population: 13709, ageBands: { "0-17": 3340, "18-29": 2742, "30-59": 5271, "60+": 2356 }, sexShareFemale: 0.513458, medianAge: 34 }, + "2913903": { population: 40706, ageBands: { "0-17": 10013, "18-29": 6629, "30-59": 16610, "60+": 7454 }, sexShareFemale: 0.518646, medianAge: 36 }, + "2914000": { population: 56876, ageBands: { "0-17": 14090, "18-29": 10098, "30-59": 22601, "60+": 10087 }, sexShareFemale: 0.517002, medianAge: 35 }, + "2914109": { population: 9935, ageBands: { "0-17": 2275, "18-29": 1678, "30-59": 4006, "60+": 1976 }, sexShareFemale: 0.501963, medianAge: 37 }, + "2914208": { population: 6101, ageBands: { "0-17": 1547, "18-29": 1003, "30-59": 2425, "60+": 1126 }, sexShareFemale: 0.506966, medianAge: 36 }, + "2914307": { population: 10752, ageBands: { "0-17": 2585, "18-29": 2078, "30-59": 4249, "60+": 1840 }, sexShareFemale: 0.489676, medianAge: 34 }, + "2914406": { population: 23879, ageBands: { "0-17": 6517, "18-29": 4791, "30-59": 9188, "60+": 3383 }, sexShareFemale: 0.505046, medianAge: 31 }, + "2914505": { population: 28043, ageBands: { "0-17": 6939, "18-29": 4758, "30-59": 11601, "60+": 4745 }, sexShareFemale: 0.519987, medianAge: 35 }, + "2914604": { population: 74507, ageBands: { "0-17": 19554, "18-29": 14739, "30-59": 31361, "60+": 8853 }, sexShareFemale: 0.520434, medianAge: 32 }, + "2914653": { population: 28165, ageBands: { "0-17": 8426, "18-29": 5265, "30-59": 10868, "60+": 3606 }, sexShareFemale: 0.496574, medianAge: 30 }, + "2914703": { population: 65073, ageBands: { "0-17": 16627, "18-29": 11807, "30-59": 26992, "60+": 9647 }, sexShareFemale: 0.517342, medianAge: 34 }, + "2914802": { population: 186708, ageBands: { "0-17": 41498, "18-29": 31997, "30-59": 80428, "60+": 32785 }, sexShareFemale: 0.534664, medianAge: 37 }, + "2914901": { population: 27704, ageBands: { "0-17": 8109, "18-29": 5368, "30-59": 11131, "60+": 3096 }, sexShareFemale: 0.491734, medianAge: 30 }, + "2915007": { population: 13472, ageBands: { "0-17": 3847, "18-29": 2429, "30-59": 5006, "60+": 2190 }, sexShareFemale: 0.499926, medianAge: 32 }, + "2915106": { population: 13803, ageBands: { "0-17": 3525, "18-29": 2365, "30-59": 5371, "60+": 2542 }, sexShareFemale: 0.496486, medianAge: 35 }, + "2915205": { population: 15310, ageBands: { "0-17": 3861, "18-29": 2512, "30-59": 6167, "60+": 2770 }, sexShareFemale: 0.497061, medianAge: 36 }, + "2915304": { population: 6347, ageBands: { "0-17": 1698, "18-29": 989, "30-59": 2518, "60+": 1142 }, sexShareFemale: 0.500867, medianAge: 35 }, + "2915353": { population: 12311, ageBands: { "0-17": 3611, "18-29": 2180, "30-59": 4708, "60+": 1812 }, sexShareFemale: 0.488100, medianAge: 32 }, + "2915403": { population: 6037, ageBands: { "0-17": 1783, "18-29": 1013, "30-59": 2254, "60+": 987 }, sexShareFemale: 0.504887, medianAge: 33 }, + "2915502": { population: 18781, ageBands: { "0-17": 4511, "18-29": 3011, "30-59": 7697, "60+": 3562 }, sexShareFemale: 0.512380, medianAge: 37 }, + "2915601": { population: 59605, ageBands: { "0-17": 15869, "18-29": 10570, "30-59": 23920, "60+": 9246 }, sexShareFemale: 0.507256, medianAge: 34 }, + "2915700": { population: 7051, ageBands: { "0-17": 1815, "18-29": 1140, "30-59": 2777, "60+": 1319 }, sexShareFemale: 0.498227, medianAge: 36 }, + "2915809": { population: 24394, ageBands: { "0-17": 5959, "18-29": 4365, "30-59": 10285, "60+": 3785 }, sexShareFemale: 0.495778, medianAge: 35 }, + "2915908": { population: 5914, ageBands: { "0-17": 1652, "18-29": 1198, "30-59": 2263, "60+": 801 }, sexShareFemale: 0.509469, medianAge: 31 }, + "2916005": { population: 17813, ageBands: { "0-17": 3920, "18-29": 2723, "30-59": 7261, "60+": 3909 }, sexShareFemale: 0.495762, medianAge: 39 }, + "2916104": { population: 19789, ageBands: { "0-17": 4613, "18-29": 3378, "30-59": 8352, "60+": 3446 }, sexShareFemale: 0.528122, medianAge: 37 }, + "2916203": { population: 10341, ageBands: { "0-17": 2572, "18-29": 1712, "30-59": 4353, "60+": 1704 }, sexShareFemale: 0.498308, medianAge: 35 }, + "2916302": { population: 9174, ageBands: { "0-17": 2566, "18-29": 1510, "30-59": 3560, "60+": 1538 }, sexShareFemale: 0.494441, medianAge: 34 }, + "2916401": { population: 65897, ageBands: { "0-17": 16587, "18-29": 11562, "30-59": 27423, "60+": 10325 }, sexShareFemale: 0.519432, medianAge: 35 }, + "2916500": { population: 31679, ageBands: { "0-17": 8697, "18-29": 5834, "30-59": 12525, "60+": 4623 }, sexShareFemale: 0.493860, medianAge: 32 }, + "2916609": { population: 10279, ageBands: { "0-17": 2682, "18-29": 1808, "30-59": 4156, "60+": 1633 }, sexShareFemale: 0.494503, medianAge: 34 }, + "2916708": { population: 8153, ageBands: { "0-17": 1819, "18-29": 1424, "30-59": 3521, "60+": 1389 }, sexShareFemale: 0.500061, medianAge: 36 }, + "2916807": { population: 17052, ageBands: { "0-17": 4231, "18-29": 2791, "30-59": 6981, "60+": 3049 }, sexShareFemale: 0.492376, medianAge: 36 }, + "2916856": { population: 15737, ageBands: { "0-17": 4338, "18-29": 2973, "30-59": 6349, "60+": 2077 }, sexShareFemale: 0.511978, medianAge: 32 }, + "2916906": { population: 10999, ageBands: { "0-17": 2560, "18-29": 1850, "30-59": 4439, "60+": 2150 }, sexShareFemale: 0.516956, medianAge: 37 }, + "2917003": { population: 33872, ageBands: { "0-17": 9083, "18-29": 6267, "30-59": 13088, "60+": 5434 }, sexShareFemale: 0.495867, medianAge: 33 }, + "2917102": { population: 16617, ageBands: { "0-17": 4082, "18-29": 2689, "30-59": 6505, "60+": 3341 }, sexShareFemale: 0.509117, medianAge: 37 }, + "2917201": { population: 17914, ageBands: { "0-17": 4163, "18-29": 3150, "30-59": 7420, "60+": 3181 }, sexShareFemale: 0.501563, medianAge: 36 }, + "2917300": { population: 21902, ageBands: { "0-17": 5809, "18-29": 3684, "30-59": 8980, "60+": 3429 }, sexShareFemale: 0.516209, medianAge: 34 }, + "2917334": { population: 11118, ageBands: { "0-17": 2888, "18-29": 2216, "30-59": 4487, "60+": 1527 }, sexShareFemale: 0.495413, medianAge: 32 }, + "2917359": { population: 9275, ageBands: { "0-17": 2322, "18-29": 1487, "30-59": 3831, "60+": 1635 }, sexShareFemale: 0.486038, medianAge: 35 }, + "2917409": { population: 14436, ageBands: { "0-17": 2801, "18-29": 2381, "30-59": 6056, "60+": 3198 }, sexShareFemale: 0.489748, medianAge: 39 }, + "2917508": { population: 82590, ageBands: { "0-17": 21086, "18-29": 14803, "30-59": 34410, "60+": 12291 }, sexShareFemale: 0.522158, medianAge: 34 }, + "2917607": { population: 45964, ageBands: { "0-17": 12205, "18-29": 8336, "30-59": 18135, "60+": 7288 }, sexShareFemale: 0.517253, medianAge: 34 }, + "2917706": { population: 32703, ageBands: { "0-17": 7973, "18-29": 5743, "30-59": 13636, "60+": 5351 }, sexShareFemale: 0.498272, medianAge: 35 }, + "2917805": { population: 17659, ageBands: { "0-17": 4431, "18-29": 3137, "30-59": 7506, "60+": 2585 }, sexShareFemale: 0.500595, medianAge: 34 }, + "2917904": { population: 9285, ageBands: { "0-17": 2818, "18-29": 1798, "30-59": 3472, "60+": 1197 }, sexShareFemale: 0.499192, medianAge: 30 }, + "2918001": { population: 158813, ageBands: { "0-17": 39567, "18-29": 27462, "30-59": 66669, "60+": 25115 }, sexShareFemale: 0.520789, medianAge: 35 }, + "2918100": { population: 37626, ageBands: { "0-17": 9769, "18-29": 6876, "30-59": 15000, "60+": 5981 }, sexShareFemale: 0.505209, medianAge: 34 }, + "2918209": { population: 13629, ageBands: { "0-17": 3286, "18-29": 2347, "30-59": 5820, "60+": 2176 }, sexShareFemale: 0.500624, medianAge: 35 }, + "2918308": { population: 14355, ageBands: { "0-17": 3548, "18-29": 2474, "30-59": 5730, "60+": 2603 }, sexShareFemale: 0.493487, medianAge: 35 }, + "2918357": { population: 24854, ageBands: { "0-17": 7320, "18-29": 4621, "30-59": 9648, "60+": 3265 }, sexShareFemale: 0.498954, medianAge: 31 }, + "2918407": { population: 237821, ageBands: { "0-17": 68289, "18-29": 48381, "30-59": 95014, "60+": 26137 }, sexShareFemale: 0.511914, medianAge: 30 }, + "2918456": { population: 9655, ageBands: { "0-17": 2533, "18-29": 1560, "30-59": 3885, "60+": 1677 }, sexShareFemale: 0.473123, medianAge: 35 }, + "2918506": { population: 16354, ageBands: { "0-17": 4443, "18-29": 2793, "30-59": 6498, "60+": 2620 }, sexShareFemale: 0.486976, medianAge: 34 }, + "2918555": { population: 5888, ageBands: { "0-17": 1439, "18-29": 995, "30-59": 2344, "60+": 1110 }, sexShareFemale: 0.496433, medianAge: 36 }, + "2918605": { population: 7379, ageBands: { "0-17": 1374, "18-29": 1067, "30-59": 3183, "60+": 1755 }, sexShareFemale: 0.510367, medianAge: 41 }, + "2918704": { population: 4075, ageBands: { "0-17": 894, "18-29": 599, "30-59": 1672, "60+": 910 }, sexShareFemale: 0.489325, medianAge: 38 }, + "2918753": { population: 14105, ageBands: { "0-17": 3198, "18-29": 2816, "30-59": 5777, "60+": 2314 }, sexShareFemale: 0.490890, medianAge: 34 }, + "2918803": { population: 21052, ageBands: { "0-17": 5178, "18-29": 3687, "30-59": 9121, "60+": 3066 }, sexShareFemale: 0.502280, medianAge: 35 }, + "2918902": { population: 3845, ageBands: { "0-17": 946, "18-29": 555, "30-59": 1574, "60+": 770 }, sexShareFemale: 0.507932, medianAge: 38 }, + "2919009": { population: 3527, ageBands: { "0-17": 904, "18-29": 558, "30-59": 1456, "60+": 609 }, sexShareFemale: 0.502694, medianAge: 36 }, + "2919058": { population: 7494, ageBands: { "0-17": 1772, "18-29": 1277, "30-59": 3113, "60+": 1332 }, sexShareFemale: 0.500133, medianAge: 36 }, + "2919108": { population: 9015, ageBands: { "0-17": 2268, "18-29": 1616, "30-59": 3556, "60+": 1575 }, sexShareFemale: 0.513810, medianAge: 34 }, + "2919157": { population: 25736, ageBands: { "0-17": 6857, "18-29": 4734, "30-59": 10256, "60+": 3889 }, sexShareFemale: 0.501515, medianAge: 33 }, + "2919207": { population: 203331, ageBands: { "0-17": 47931, "18-29": 35337, "30-59": 94593, "60+": 25470 }, sexShareFemale: 0.531385, medianAge: 35 }, + "2919306": { population: 10774, ageBands: { "0-17": 3007, "18-29": 2091, "30-59": 4440, "60+": 1236 }, sexShareFemale: 0.507240, medianAge: 31 }, + "2919405": { population: 11834, ageBands: { "0-17": 2671, "18-29": 1776, "30-59": 4863, "60+": 2524 }, sexShareFemale: 0.494761, medianAge: 38 }, + "2919504": { population: 43903, ageBands: { "0-17": 9955, "18-29": 7583, "30-59": 18979, "60+": 7386 }, sexShareFemale: 0.499009, medianAge: 36 }, + "2919553": { population: 107909, ageBands: { "0-17": 32888, "18-29": 25039, "30-59": 44439, "60+": 5543 }, sexShareFemale: 0.498513, medianAge: 28 }, + "2919603": { population: 10454, ageBands: { "0-17": 2821, "18-29": 1676, "30-59": 4056, "60+": 1901 }, sexShareFemale: 0.511575, medianAge: 35 }, + "2919702": { population: 21599, ageBands: { "0-17": 4843, "18-29": 3752, "30-59": 9849, "60+": 3155 }, sexShareFemale: 0.512616, medianAge: 35 }, + "2919801": { population: 41859, ageBands: { "0-17": 10015, "18-29": 7669, "30-59": 16536, "60+": 7639 }, sexShareFemale: 0.503787, medianAge: 35 }, + "2919900": { population: 7256, ageBands: { "0-17": 2038, "18-29": 1286, "30-59": 2739, "60+": 1193 }, sexShareFemale: 0.499862, medianAge: 33 }, + "2919926": { population: 18504, ageBands: { "0-17": 4347, "18-29": 3267, "30-59": 8282, "60+": 2608 }, sexShareFemale: 0.530588, medianAge: 36 }, + "2919959": { population: 6973, ageBands: { "0-17": 1536, "18-29": 1092, "30-59": 2799, "60+": 1546 }, sexShareFemale: 0.493331, medianAge: 39 }, + "2920007": { population: 8731, ageBands: { "0-17": 2221, "18-29": 1546, "30-59": 3531, "60+": 1433 }, sexShareFemale: 0.499256, medianAge: 35 }, + "2920106": { population: 17674, ageBands: { "0-17": 4024, "18-29": 3066, "30-59": 7094, "60+": 3490 }, sexShareFemale: 0.503791, medianAge: 37 }, + "2920205": { population: 15398, ageBands: { "0-17": 4502, "18-29": 2851, "30-59": 5973, "60+": 2072 }, sexShareFemale: 0.490843, medianAge: 31 }, + "2920304": { population: 8670, ageBands: { "0-17": 1942, "18-29": 1489, "30-59": 3629, "60+": 1610 }, sexShareFemale: 0.490081, medianAge: 37 }, + "2920403": { population: 13860, ageBands: { "0-17": 3453, "18-29": 2404, "30-59": 5365, "60+": 2638 }, sexShareFemale: 0.493939, medianAge: 36 }, + "2920452": { population: 13919, ageBands: { "0-17": 3516, "18-29": 2801, "30-59": 5682, "60+": 1920 }, sexShareFemale: 0.485523, medianAge: 32 }, + "2920502": { population: 27620, ageBands: { "0-17": 6700, "18-29": 4997, "30-59": 11364, "60+": 4559 }, sexShareFemale: 0.509124, medianAge: 35 }, + "2920601": { population: 35859, ageBands: { "0-17": 8500, "18-29": 6268, "30-59": 15677, "60+": 5414 }, sexShareFemale: 0.511726, medianAge: 36 }, + "2920700": { population: 24527, ageBands: { "0-17": 7062, "18-29": 4917, "30-59": 9996, "60+": 2552 }, sexShareFemale: 0.489787, medianAge: 30 }, + "2920809": { population: 9267, ageBands: { "0-17": 2477, "18-29": 1640, "30-59": 3636, "60+": 1514 }, sexShareFemale: 0.500917, medianAge: 33 }, + "2920908": { population: 13544, ageBands: { "0-17": 3547, "18-29": 2364, "30-59": 5230, "60+": 2403 }, sexShareFemale: 0.496899, medianAge: 34 }, + "2921005": { population: 42566, ageBands: { "0-17": 11187, "18-29": 7588, "30-59": 18313, "60+": 5478 }, sexShareFemale: 0.522107, medianAge: 34 }, + "2921054": { population: 10330, ageBands: { "0-17": 2678, "18-29": 1969, "30-59": 4058, "60+": 1625 }, sexShareFemale: 0.497967, medianAge: 33 }, + "2921104": { population: 22194, ageBands: { "0-17": 5479, "18-29": 3626, "30-59": 8949, "60+": 4140 }, sexShareFemale: 0.495720, medianAge: 36 }, + "2921203": { population: 24661, ageBands: { "0-17": 6236, "18-29": 3930, "30-59": 9963, "60+": 4532 }, sexShareFemale: 0.507765, medianAge: 36 }, + "2921302": { population: 11071, ageBands: { "0-17": 2822, "18-29": 2024, "30-59": 4438, "60+": 1787 }, sexShareFemale: 0.512691, medianAge: 34 }, + "2921401": { population: 15734, ageBands: { "0-17": 4141, "18-29": 2772, "30-59": 6251, "60+": 2570 }, sexShareFemale: 0.500636, medianAge: 34 }, + "2921450": { population: 10187, ageBands: { "0-17": 2391, "18-29": 1783, "30-59": 4122, "60+": 1891 }, sexShareFemale: 0.483656, medianAge: 36 }, + "2921500": { population: 47780, ageBands: { "0-17": 12185, "18-29": 8290, "30-59": 19131, "60+": 8174 }, sexShareFemale: 0.493428, medianAge: 35 }, + "2921609": { population: 7996, ageBands: { "0-17": 2139, "18-29": 1298, "30-59": 3016, "60+": 1543 }, sexShareFemale: 0.487869, medianAge: 35 }, + "2921708": { population: 33594, ageBands: { "0-17": 9427, "18-29": 5745, "30-59": 13183, "60+": 5239 }, sexShareFemale: 0.505299, medianAge: 33 }, + "2921807": { population: 11143, ageBands: { "0-17": 2504, "18-29": 1500, "30-59": 4578, "60+": 2561 }, sexShareFemale: 0.507224, medianAge: 40 }, + "2921906": { population: 12137, ageBands: { "0-17": 3241, "18-29": 2191, "30-59": 5019, "60+": 1686 }, sexShareFemale: 0.506056, medianAge: 33 }, + "2922003": { population: 37977, ageBands: { "0-17": 10822, "18-29": 6829, "30-59": 15506, "60+": 4820 }, sexShareFemale: 0.504832, medianAge: 32 }, + "2922052": { population: 13152, ageBands: { "0-17": 3805, "18-29": 2729, "30-59": 5033, "60+": 1585 }, sexShareFemale: 0.501597, medianAge: 30 }, + "2922102": { population: 17305, ageBands: { "0-17": 4459, "18-29": 2901, "30-59": 6778, "60+": 3167 }, sexShareFemale: 0.507252, medianAge: 36 }, + "2922201": { population: 7190, ageBands: { "0-17": 1522, "18-29": 1134, "30-59": 3306, "60+": 1228 }, sexShareFemale: 0.507789, medianAge: 38 }, + "2922250": { population: 10443, ageBands: { "0-17": 3178, "18-29": 2057, "30-59": 3893, "60+": 1315 }, sexShareFemale: 0.473044, medianAge: 29 }, + "2922300": { population: 28707, ageBands: { "0-17": 6765, "18-29": 4908, "30-59": 12441, "60+": 4593 }, sexShareFemale: 0.533285, medianAge: 36 }, + "2922409": { population: 20037, ageBands: { "0-17": 4852, "18-29": 3478, "30-59": 8468, "60+": 3239 }, sexShareFemale: 0.519289, medianAge: 35 }, + "2922508": { population: 27060, ageBands: { "0-17": 6498, "18-29": 4648, "30-59": 11894, "60+": 4020 }, sexShareFemale: 0.518514, medianAge: 35 }, + "2922607": { population: 12063, ageBands: { "0-17": 3331, "18-29": 2057, "30-59": 4947, "60+": 1728 }, sexShareFemale: 0.495648, medianAge: 34 }, + "2922656": { population: 18523, ageBands: { "0-17": 4867, "18-29": 3678, "30-59": 7491, "60+": 2487 }, sexShareFemale: 0.498029, medianAge: 32 }, + "2922706": { population: 13715, ageBands: { "0-17": 3296, "18-29": 2185, "30-59": 5469, "60+": 2765 }, sexShareFemale: 0.496610, medianAge: 38 }, + "2922730": { population: 7967, ageBands: { "0-17": 1716, "18-29": 1275, "30-59": 3406, "60+": 1570 }, sexShareFemale: 0.510983, medianAge: 39 }, + "2922755": { population: 6501, ageBands: { "0-17": 1703, "18-29": 1000, "30-59": 2724, "60+": 1074 }, sexShareFemale: 0.483618, medianAge: 36 }, + "2922805": { population: 7780, ageBands: { "0-17": 2183, "18-29": 1503, "30-59": 2955, "60+": 1139 }, sexShareFemale: 0.494216, medianAge: 31 }, + "2922854": { population: 7538, ageBands: { "0-17": 1977, "18-29": 1419, "30-59": 2844, "60+": 1298 }, sexShareFemale: 0.497745, medianAge: 33 }, + "2922904": { population: 24236, ageBands: { "0-17": 6307, "18-29": 4296, "30-59": 9425, "60+": 4208 }, sexShareFemale: 0.502311, medianAge: 35 }, + "2923001": { population: 39509, ageBands: { "0-17": 11496, "18-29": 7119, "30-59": 15332, "60+": 5562 }, sexShareFemale: 0.509099, medianAge: 32 }, + "2923035": { population: 11162, ageBands: { "0-17": 2765, "18-29": 1995, "30-59": 4562, "60+": 1840 }, sexShareFemale: 0.492564, medianAge: 34 }, + "2923050": { population: 10660, ageBands: { "0-17": 2553, "18-29": 1975, "30-59": 4337, "60+": 1795 }, sexShareFemale: 0.505535, medianAge: 35 }, + "2923100": { population: 22633, ageBands: { "0-17": 6014, "18-29": 3938, "30-59": 9076, "60+": 3605 }, sexShareFemale: 0.513145, medianAge: 34 }, + "2923209": { population: 20715, ageBands: { "0-17": 5029, "18-29": 3622, "30-59": 7991, "60+": 4073 }, sexShareFemale: 0.494376, medianAge: 36 }, + "2923308": { population: 7716, ageBands: { "0-17": 1842, "18-29": 1307, "30-59": 3185, "60+": 1382 }, sexShareFemale: 0.518015, medianAge: 36 }, + "2923357": { population: 19243, ageBands: { "0-17": 5665, "18-29": 3511, "30-59": 7669, "60+": 2398 }, sexShareFemale: 0.495609, medianAge: 31 }, + "2923407": { population: 20078, ageBands: { "0-17": 4730, "18-29": 3444, "30-59": 8236, "60+": 3668 }, sexShareFemale: 0.494422, medianAge: 36 }, + "2923506": { population: 10339, ageBands: { "0-17": 2786, "18-29": 1920, "30-59": 4185, "60+": 1448 }, sexShareFemale: 0.506239, medianAge: 32 }, + "2923605": { population: 20351, ageBands: { "0-17": 4294, "18-29": 3506, "30-59": 8616, "60+": 3935 }, sexShareFemale: 0.502825, medianAge: 37 }, + "2923704": { population: 29252, ageBands: { "0-17": 8621, "18-29": 5050, "30-59": 11103, "60+": 4478 }, sexShareFemale: 0.497675, medianAge: 32 }, + "2923803": { population: 26604, ageBands: { "0-17": 6229, "18-29": 5000, "30-59": 10888, "60+": 4487 }, sexShareFemale: 0.509585, medianAge: 34 }, + "2923902": { population: 9370, ageBands: { "0-17": 2815, "18-29": 1584, "30-59": 3473, "60+": 1498 }, sexShareFemale: 0.493703, medianAge: 32 }, + "2924009": { population: 112870, ageBands: { "0-17": 29258, "18-29": 20648, "30-59": 46732, "60+": 16232 }, sexShareFemale: 0.524311, medianAge: 33 }, + "2924058": { population: 13243, ageBands: { "0-17": 3196, "18-29": 2154, "30-59": 5470, "60+": 2423 }, sexShareFemale: 0.509703, medianAge: 36 }, + "2924108": { population: 6235, ageBands: { "0-17": 1540, "18-29": 1119, "30-59": 2427, "60+": 1149 }, sexShareFemale: 0.517723, medianAge: 35 }, + "2924207": { population: 13954, ageBands: { "0-17": 4077, "18-29": 2843, "30-59": 5456, "60+": 1578 }, sexShareFemale: 0.498208, medianAge: 30 }, + "2924306": { population: 20086, ageBands: { "0-17": 5112, "18-29": 3307, "30-59": 7758, "60+": 3909 }, sexShareFemale: 0.513641, medianAge: 35 }, + "2924405": { population: 35357, ageBands: { "0-17": 11121, "18-29": 6421, "30-59": 12900, "60+": 4915 }, sexShareFemale: 0.490115, medianAge: 30 }, + "2924504": { population: 14731, ageBands: { "0-17": 3423, "18-29": 2404, "30-59": 6039, "60+": 2865 }, sexShareFemale: 0.492838, medianAge: 37 }, + "2924603": { population: 19083, ageBands: { "0-17": 5261, "18-29": 3398, "30-59": 7532, "60+": 2892 }, sexShareFemale: 0.503432, medianAge: 33 }, + "2924652": { population: 10325, ageBands: { "0-17": 2336, "18-29": 1957, "30-59": 4076, "60+": 1956 }, sexShareFemale: 0.500726, medianAge: 35 }, + "2924678": { population: 10974, ageBands: { "0-17": 2825, "18-29": 2283, "30-59": 4532, "60+": 1334 }, sexShareFemale: 0.487425, medianAge: 32 }, + "2924702": { population: 9152, ageBands: { "0-17": 2103, "18-29": 1337, "30-59": 3677, "60+": 2035 }, sexShareFemale: 0.510708, medianAge: 39 }, + "2924801": { population: 17566, ageBands: { "0-17": 4461, "18-29": 2900, "30-59": 6898, "60+": 3307 }, sexShareFemale: 0.507799, medianAge: 36 }, + "2924900": { population: 8022, ageBands: { "0-17": 1897, "18-29": 1268, "30-59": 3310, "60+": 1547 }, sexShareFemale: 0.485914, medianAge: 37 }, + "2925006": { population: 23334, ageBands: { "0-17": 5885, "18-29": 3993, "30-59": 9442, "60+": 4014 }, sexShareFemale: 0.515514, medianAge: 35 }, + "2925105": { population: 48293, ageBands: { "0-17": 12384, "18-29": 8718, "30-59": 18642, "60+": 8549 }, sexShareFemale: 0.512435, medianAge: 34 }, + "2925204": { population: 32136, ageBands: { "0-17": 7837, "18-29": 5429, "30-59": 14450, "60+": 4420 }, sexShareFemale: 0.531927, medianAge: 35 }, + "2925253": { population: 17938, ageBands: { "0-17": 4911, "18-29": 3367, "30-59": 7008, "60+": 2652 }, sexShareFemale: 0.505853, medianAge: 32 }, + "2925303": { population: 168326, ageBands: { "0-17": 48481, "18-29": 34456, "30-59": 69723, "60+": 15666 }, sexShareFemale: 0.508715, medianAge: 30 }, + "2925402": { population: 10274, ageBands: { "0-17": 2762, "18-29": 1864, "30-59": 4083, "60+": 1565 }, sexShareFemale: 0.495523, medianAge: 33 }, + "2925501": { population: 35003, ageBands: { "0-17": 10352, "18-29": 6554, "30-59": 13341, "60+": 4756 }, sexShareFemale: 0.494243, medianAge: 31 }, + "2925600": { population: 15130, ageBands: { "0-17": 3544, "18-29": 2489, "30-59": 6537, "60+": 2560 }, sexShareFemale: 0.498612, medianAge: 37 }, + "2925709": { population: 12621, ageBands: { "0-17": 2808, "18-29": 2077, "30-59": 5104, "60+": 2632 }, sexShareFemale: 0.491245, medianAge: 38 }, + "2925758": { population: 27734, ageBands: { "0-17": 7897, "18-29": 5445, "30-59": 11065, "60+": 3327 }, sexShareFemale: 0.495637, medianAge: 31 }, + "2925808": { population: 25988, ageBands: { "0-17": 6522, "18-29": 4758, "30-59": 10251, "60+": 4457 }, sexShareFemale: 0.506195, medianAge: 35 }, + "2925907": { population: 25272, ageBands: { "0-17": 6288, "18-29": 4451, "30-59": 9967, "60+": 4566 }, sexShareFemale: 0.489316, medianAge: 35 }, + "2925931": { population: 9461, ageBands: { "0-17": 2183, "18-29": 1616, "30-59": 3863, "60+": 1799 }, sexShareFemale: 0.495719, medianAge: 36 }, + "2925956": { population: 19662, ageBands: { "0-17": 4992, "18-29": 3504, "30-59": 7482, "60+": 3684 }, sexShareFemale: 0.518309, medianAge: 35 }, + "2926004": { population: 40586, ageBands: { "0-17": 10952, "18-29": 7109, "30-59": 16078, "60+": 6447 }, sexShareFemale: 0.501331, medianAge: 34 }, + "2926103": { population: 13651, ageBands: { "0-17": 3480, "18-29": 2276, "30-59": 5693, "60+": 2202 }, sexShareFemale: 0.510219, medianAge: 35 }, + "2926202": { population: 21642, ageBands: { "0-17": 6076, "18-29": 3692, "30-59": 8501, "60+": 3373 }, sexShareFemale: 0.478837, medianAge: 33 }, + "2926301": { population: 33386, ageBands: { "0-17": 7655, "18-29": 5620, "30-59": 13929, "60+": 6182 }, sexShareFemale: 0.515366, medianAge: 37 }, + "2926400": { population: 30711, ageBands: { "0-17": 7510, "18-29": 5484, "30-59": 12379, "60+": 5338 }, sexShareFemale: 0.497346, medianAge: 35 }, + "2926509": { population: 13841, ageBands: { "0-17": 3587, "18-29": 2669, "30-59": 5413, "60+": 2172 }, sexShareFemale: 0.492450, medianAge: 33 }, + "2926608": { population: 54010, ageBands: { "0-17": 13519, "18-29": 10133, "30-59": 21835, "60+": 8523 }, sexShareFemale: 0.511757, medianAge: 34 }, + "2926657": { population: 9740, ageBands: { "0-17": 2538, "18-29": 1690, "30-59": 3933, "60+": 1579 }, sexShareFemale: 0.473717, medianAge: 35 }, + "2926707": { population: 13184, ageBands: { "0-17": 2762, "18-29": 1993, "30-59": 5449, "60+": 2980 }, sexShareFemale: 0.509481, medianAge: 39 }, + "2926806": { population: 13146, ageBands: { "0-17": 2799, "18-29": 2219, "30-59": 5536, "60+": 2592 }, sexShareFemale: 0.498479, medianAge: 38 }, + "2926905": { population: 10497, ageBands: { "0-17": 2229, "18-29": 1763, "30-59": 4274, "60+": 2231 }, sexShareFemale: 0.508717, medianAge: 38 }, + "2927002": { population: 35362, ageBands: { "0-17": 9518, "18-29": 6450, "30-59": 14363, "60+": 5031 }, sexShareFemale: 0.504072, medianAge: 33 }, + "2927101": { population: 10308, ageBands: { "0-17": 3349, "18-29": 2105, "30-59": 3795, "60+": 1059 }, sexShareFemale: 0.507179, medianAge: 28 }, + "2927200": { population: 28282, ageBands: { "0-17": 7219, "18-29": 5250, "30-59": 10954, "60+": 4859 }, sexShareFemale: 0.521993, medianAge: 34 }, + "2927309": { population: 14987, ageBands: { "0-17": 3898, "18-29": 2494, "30-59": 6295, "60+": 2300 }, sexShareFemale: 0.521652, medianAge: 35 }, + "2927408": { population: 2417678, ageBands: { "0-17": 493892, "18-29": 413081, "30-59": 1111630, "60+": 399075 }, sexShareFemale: 0.544034, medianAge: 38 }, + "2927507": { population: 20952, ageBands: { "0-17": 5218, "18-29": 4008, "30-59": 8476, "60+": 3250 }, sexShareFemale: 0.507875, medianAge: 34 }, + "2927606": { population: 14965, ageBands: { "0-17": 4060, "18-29": 2670, "30-59": 5997, "60+": 2238 }, sexShareFemale: 0.496893, medianAge: 33 }, + "2927705": { population: 29185, ageBands: { "0-17": 8322, "18-29": 5141, "30-59": 12032, "60+": 3690 }, sexShareFemale: 0.504163, medianAge: 32 }, + "2927804": { population: 4681, ageBands: { "0-17": 1200, "18-29": 747, "30-59": 1831, "60+": 903 }, sexShareFemale: 0.500748, medianAge: 36 }, + "2927903": { population: 10300, ageBands: { "0-17": 2475, "18-29": 1760, "30-59": 4157, "60+": 1908 }, sexShareFemale: 0.505243, medianAge: 36 }, + "2928000": { population: 37834, ageBands: { "0-17": 9878, "18-29": 7021, "30-59": 15192, "60+": 5743 }, sexShareFemale: 0.506238, medianAge: 33 }, + "2928059": { population: 13896, ageBands: { "0-17": 3756, "18-29": 2419, "30-59": 5710, "60+": 2011 }, sexShareFemale: 0.474669, medianAge: 33 }, + "2928109": { population: 38604, ageBands: { "0-17": 9952, "18-29": 6711, "30-59": 15757, "60+": 6184 }, sexShareFemale: 0.500699, medianAge: 34 }, + "2928208": { population: 24755, ageBands: { "0-17": 6150, "18-29": 4178, "30-59": 9756, "60+": 4671 }, sexShareFemale: 0.491699, medianAge: 36 }, + "2928307": { population: 8716, ageBands: { "0-17": 2172, "18-29": 1504, "30-59": 3463, "60+": 1577 }, sexShareFemale: 0.525585, medianAge: 35 }, + "2928406": { population: 27390, ageBands: { "0-17": 7900, "18-29": 4755, "30-59": 10291, "60+": 4444 }, sexShareFemale: 0.488682, medianAge: 32 }, + "2928505": { population: 10441, ageBands: { "0-17": 2460, "18-29": 1813, "30-59": 4253, "60+": 1915 }, sexShareFemale: 0.516521, medianAge: 36 }, + "2928604": { population: 56012, ageBands: { "0-17": 12937, "18-29": 9190, "30-59": 24240, "60+": 9645 }, sexShareFemale: 0.529101, medianAge: 37 }, + "2928703": { population: 103055, ageBands: { "0-17": 24024, "18-29": 19620, "30-59": 46527, "60+": 12884 }, sexShareFemale: 0.533404, medianAge: 34 }, + "2928802": { population: 52276, ageBands: { "0-17": 14362, "18-29": 9903, "30-59": 20635, "60+": 7376 }, sexShareFemale: 0.525710, medianAge: 32 }, + "2928901": { population: 32828, ageBands: { "0-17": 9933, "18-29": 6779, "30-59": 12659, "60+": 3457 }, sexShareFemale: 0.485957, medianAge: 29 }, + "2928950": { population: 8426, ageBands: { "0-17": 1860, "18-29": 1308, "30-59": 3544, "60+": 1714 }, sexShareFemale: 0.508782, medianAge: 39 }, + "2929008": { population: 11026, ageBands: { "0-17": 2535, "18-29": 1896, "30-59": 4763, "60+": 1832 }, sexShareFemale: 0.522492, medianAge: 37 }, + "2929057": { population: 15194, ageBands: { "0-17": 4161, "18-29": 2746, "30-59": 6156, "60+": 2131 }, sexShareFemale: 0.500856, medianAge: 33 }, + "2929107": { population: 20283, ageBands: { "0-17": 4511, "18-29": 3369, "30-59": 8968, "60+": 3435 }, sexShareFemale: 0.511956, medianAge: 37 }, + "2929206": { population: 38733, ageBands: { "0-17": 10176, "18-29": 7243, "30-59": 16858, "60+": 4456 }, sexShareFemale: 0.529987, medianAge: 33 }, + "2929255": { population: 18600, ageBands: { "0-17": 4879, "18-29": 3238, "30-59": 7572, "60+": 2911 }, sexShareFemale: 0.492097, medianAge: 34 }, + "2929305": { population: 39513, ageBands: { "0-17": 10143, "18-29": 7110, "30-59": 17096, "60+": 5164 }, sexShareFemale: 0.527953, medianAge: 34 }, + "2929354": { population: 5315, ageBands: { "0-17": 1392, "18-29": 856, "30-59": 2106, "60+": 961 }, sexShareFemale: 0.496143, medianAge: 35 }, + "2929370": { population: 10187, ageBands: { "0-17": 2662, "18-29": 1744, "30-59": 4021, "60+": 1760 }, sexShareFemale: 0.505252, medianAge: 35 }, + "2929404": { population: 10334, ageBands: { "0-17": 2343, "18-29": 1877, "30-59": 4357, "60+": 1757 }, sexShareFemale: 0.500194, medianAge: 36 }, + "2929503": { population: 40958, ageBands: { "0-17": 9638, "18-29": 6922, "30-59": 18061, "60+": 6337 }, sexShareFemale: 0.526515, medianAge: 36 }, + "2929602": { population: 17963, ageBands: { "0-17": 4220, "18-29": 3154, "30-59": 7793, "60+": 2796 }, sexShareFemale: 0.517787, medianAge: 35 }, + "2929701": { population: 16008, ageBands: { "0-17": 4337, "18-29": 3039, "30-59": 6143, "60+": 2489 }, sexShareFemale: 0.505185, medianAge: 33 }, + "2929750": { population: 11438, ageBands: { "0-17": 2680, "18-29": 1702, "30-59": 4912, "60+": 2144 }, sexShareFemale: 0.525616, medianAge: 38 }, + "2929800": { population: 10478, ageBands: { "0-17": 2676, "18-29": 1648, "30-59": 4134, "60+": 2020 }, sexShareFemale: 0.511739, medianAge: 37 }, + "2929909": { population: 46160, ageBands: { "0-17": 12172, "18-29": 9073, "30-59": 18310, "60+": 6605 }, sexShareFemale: 0.512630, medianAge: 32 }, + "2930006": { population: 9360, ageBands: { "0-17": 2288, "18-29": 1482, "30-59": 3887, "60+": 1703 }, sexShareFemale: 0.494872, medianAge: 36 }, + "2930105": { population: 74523, ageBands: { "0-17": 18886, "18-29": 13064, "30-59": 30939, "60+": 11634 }, sexShareFemale: 0.522000, medianAge: 35 }, + "2930154": { population: 34222, ageBands: { "0-17": 10616, "18-29": 6288, "30-59": 12769, "60+": 4549 }, sexShareFemale: 0.492344, medianAge: 30 }, + "2930204": { population: 38154, ageBands: { "0-17": 11650, "18-29": 7029, "30-59": 14519, "60+": 4956 }, sexShareFemale: 0.484222, medianAge: 30 }, + "2930303": { population: 17066, ageBands: { "0-17": 4213, "18-29": 2601, "30-59": 7036, "60+": 3216 }, sexShareFemale: 0.487402, medianAge: 37 }, + "2930402": { population: 17996, ageBands: { "0-17": 4098, "18-29": 3260, "30-59": 7259, "60+": 3379 }, sexShareFemale: 0.516948, medianAge: 36 }, + "2930501": { population: 80435, ageBands: { "0-17": 20421, "18-29": 14858, "30-59": 33908, "60+": 11248 }, sexShareFemale: 0.521713, medianAge: 34 }, + "2930600": { population: 13335, ageBands: { "0-17": 3263, "18-29": 2123, "30-59": 5462, "60+": 2487 }, sexShareFemale: 0.511961, medianAge: 36 }, + "2930709": { population: 114559, ageBands: { "0-17": 30150, "18-29": 21250, "30-59": 49829, "60+": 13330 }, sexShareFemale: 0.525066, medianAge: 33 }, + "2930758": { population: 13408, ageBands: { "0-17": 4262, "18-29": 2584, "30-59": 4919, "60+": 1643 }, sexShareFemale: 0.486202, medianAge: 29 }, + "2930766": { population: 14773, ageBands: { "0-17": 3313, "18-29": 2879, "30-59": 6003, "60+": 2578 }, sexShareFemale: 0.492656, medianAge: 35 }, + "2930774": { population: 25475, ageBands: { "0-17": 7335, "18-29": 4907, "30-59": 9788, "60+": 3445 }, sexShareFemale: 0.510304, medianAge: 31 }, + "2930808": { population: 17054, ageBands: { "0-17": 4591, "18-29": 3574, "30-59": 6586, "60+": 2303 }, sexShareFemale: 0.500938, medianAge: 31 }, + "2930907": { population: 11979, ageBands: { "0-17": 3077, "18-29": 2168, "30-59": 4717, "60+": 2017 }, sexShareFemale: 0.484515, medianAge: 34 }, + "2931004": { population: 21006, ageBands: { "0-17": 4843, "18-29": 3502, "30-59": 8884, "60+": 3777 }, sexShareFemale: 0.498429, medianAge: 36 }, + "2931053": { population: 17158, ageBands: { "0-17": 4318, "18-29": 3272, "30-59": 6763, "60+": 2805 }, sexShareFemale: 0.501748, medianAge: 34 }, + "2931103": { population: 7717, ageBands: { "0-17": 1790, "18-29": 1441, "30-59": 3047, "60+": 1439 }, sexShareFemale: 0.514189, medianAge: 36 }, + "2931202": { population: 18044, ageBands: { "0-17": 5161, "18-29": 3267, "30-59": 7136, "60+": 2480 }, sexShareFemale: 0.501607, medianAge: 32 }, + "2931301": { population: 15818, ageBands: { "0-17": 4528, "18-29": 3137, "30-59": 6110, "60+": 2043 }, sexShareFemale: 0.504046, medianAge: 30 }, + "2931350": { population: 145216, ageBands: { "0-17": 38258, "18-29": 27127, "30-59": 60099, "60+": 19732 }, sexShareFemale: 0.517932, medianAge: 33 }, + "2931400": { population: 7110, ageBands: { "0-17": 1549, "18-29": 1254, "30-59": 3075, "60+": 1232 }, sexShareFemale: 0.517018, medianAge: 37 }, + "2931509": { population: 21176, ageBands: { "0-17": 5630, "18-29": 3878, "30-59": 8528, "60+": 3140 }, sexShareFemale: 0.507934, medianAge: 33 }, + "2931608": { population: 15332, ageBands: { "0-17": 4259, "18-29": 3162, "30-59": 5984, "60+": 1927 }, sexShareFemale: 0.485977, medianAge: 30 }, + "2931707": { population: 10798, ageBands: { "0-17": 2564, "18-29": 1991, "30-59": 4453, "60+": 1790 }, sexShareFemale: 0.525097, medianAge: 36 }, + "2931806": { population: 16296, ageBands: { "0-17": 3577, "18-29": 2457, "30-59": 6580, "60+": 3682 }, sexShareFemale: 0.496011, medianAge: 39 }, + "2931905": { population: 48736, ageBands: { "0-17": 11712, "18-29": 8456, "30-59": 20426, "60+": 8142 }, sexShareFemale: 0.504473, medianAge: 35 }, + "2932002": { population: 24665, ageBands: { "0-17": 6078, "18-29": 3846, "30-59": 10122, "60+": 4619 }, sexShareFemale: 0.495358, medianAge: 37 }, + "2932101": { population: 18626, ageBands: { "0-17": 4584, "18-29": 2884, "30-59": 7755, "60+": 3403 }, sexShareFemale: 0.500376, medianAge: 36 }, + "2932200": { population: 17596, ageBands: { "0-17": 4817, "18-29": 2960, "30-59": 6939, "60+": 2880 }, sexShareFemale: 0.519152, medianAge: 34 }, + "2932309": { population: 16094, ageBands: { "0-17": 4226, "18-29": 2569, "30-59": 6244, "60+": 3055 }, sexShareFemale: 0.516341, medianAge: 36 }, + "2932408": { population: 13432, ageBands: { "0-17": 3027, "18-29": 2220, "30-59": 5784, "60+": 2401 }, sexShareFemale: 0.494491, medianAge: 37 }, + "2932457": { population: 13642, ageBands: { "0-17": 3899, "18-29": 2529, "30-59": 5398, "60+": 1816 }, sexShareFemale: 0.489811, medianAge: 31 }, + "2932507": { population: 18131, ageBands: { "0-17": 4663, "18-29": 2805, "30-59": 7350, "60+": 3313 }, sexShareFemale: 0.498924, medianAge: 36 }, + "2932606": { population: 15355, ageBands: { "0-17": 3538, "18-29": 2654, "30-59": 6316, "60+": 2847 }, sexShareFemale: 0.499446, medianAge: 36 }, + "2932705": { population: 21420, ageBands: { "0-17": 5858, "18-29": 3578, "30-59": 8632, "60+": 3352 }, sexShareFemale: 0.506676, medianAge: 34 }, + "2932804": { population: 16277, ageBands: { "0-17": 4420, "18-29": 3058, "30-59": 6391, "60+": 2408 }, sexShareFemale: 0.504331, medianAge: 32 }, + "2932903": { population: 85655, ageBands: { "0-17": 21598, "18-29": 15646, "30-59": 36102, "60+": 12309 }, sexShareFemale: 0.518300, medianAge: 34 }, + "2933000": { population: 24362, ageBands: { "0-17": 5751, "18-29": 4019, "30-59": 10544, "60+": 4048 }, sexShareFemale: 0.514572, medianAge: 36 }, + "2933059": { population: 13800, ageBands: { "0-17": 3216, "18-29": 2383, "30-59": 5587, "60+": 2614 }, sexShareFemale: 0.506087, medianAge: 36 }, + "2933109": { population: 8101, ageBands: { "0-17": 1821, "18-29": 1277, "30-59": 3360, "60+": 1643 }, sexShareFemale: 0.508209, medianAge: 38 }, + "2933158": { population: 13377, ageBands: { "0-17": 3639, "18-29": 2296, "30-59": 5255, "60+": 2187 }, sexShareFemale: 0.501682, medianAge: 33 }, + "2933174": { population: 9913, ageBands: { "0-17": 2288, "18-29": 1704, "30-59": 4281, "60+": 1640 }, sexShareFemale: 0.502169, medianAge: 36 }, + "2933208": { population: 42529, ageBands: { "0-17": 9661, "18-29": 6943, "30-59": 18070, "60+": 7855 }, sexShareFemale: 0.516259, medianAge: 38 }, + "2933257": { population: 6003, ageBands: { "0-17": 1473, "18-29": 948, "30-59": 2415, "60+": 1167 }, sexShareFemale: 0.480260, medianAge: 37 }, + "2933307": { population: 370879, ageBands: { "0-17": 91170, "18-29": 72665, "30-59": 155901, "60+": 51143 }, sexShareFemale: 0.521647, medianAge: 33 }, + "2933406": { population: 9503, ageBands: { "0-17": 2349, "18-29": 1844, "30-59": 3800, "60+": 1510 }, sexShareFemale: 0.507840, medianAge: 33 }, + "2933455": { population: 12968, ageBands: { "0-17": 3510, "18-29": 2406, "30-59": 5052, "60+": 2000 }, sexShareFemale: 0.481801, medianAge: 33 }, + "2933505": { population: 24474, ageBands: { "0-17": 7100, "18-29": 4356, "30-59": 9740, "60+": 3278 }, sexShareFemale: 0.492972, medianAge: 32 }, + "2933604": { population: 44757, ageBands: { "0-17": 13790, "18-29": 8312, "30-59": 16418, "60+": 6237 }, sexShareFemale: 0.511026, medianAge: 30 }, +} + +export const demographicsForCode = (codarea: string): MunicipalityDemographics | undefined => + bahiaMunicipalityDemographics[codarea] + +export const demographicsForMunicipalityName = ( + municipality: string, +): MunicipalityDemographics | undefined => { + const code = codeForMunicipality(municipality) + return code ? demographicsForCode(code) : undefined +} diff --git a/src/utilities/nucleusElectionGeography.ts b/src/utilities/nucleusElectionGeography.ts index c533207f..cb664779 100644 --- a/src/utilities/nucleusElectionGeography.ts +++ b/src/utilities/nucleusElectionGeography.ts @@ -1,14 +1,13 @@ import type { Where } from 'payload' import { tseCityCodeForMunicipality } from '@/lib/bahiaTseCityCodes' -import { citiesForTerritory, isBahiaIdentityTerritory } from '@/lib/bahiaTerritories' import { tseZonesForCity } from '@/lib/bahiaTseZones' import { normalizeTerritoryTextArray } from '@/utilities/campaignTerritoryValidation' +import { resolveNucleusTerritoryCities } from '@/utilities/nucleusTerritoryCities' +import type { NucleusTerritoryCitiesInput } from '@/utilities/nucleusTerritoryCities' import { sortedUniqueZoneNumbers } from '@/utilities/tseZone' -export type NucleusElectionGeographyInput = { - cities: string[] - regions: string[] +export type NucleusElectionGeographyInput = NucleusTerritoryCitiesInput & { tseZones: number[] } @@ -35,9 +34,6 @@ export type NucleusElectionGeography = { cityZonePairs: NucleusCityZonePair[] } -const uniqueSortedCities = (values: Iterable): string[] => - [...new Set(values)].sort((left, right) => left.localeCompare(right, 'pt-BR')) - /** * Resolve the effective city×zone geography for a nucleus baseline query. * Returns null when the nucleus has no usable territory (no cities and no regions), @@ -51,16 +47,7 @@ const uniqueSortedCities = (values: Iterable): string[] => export const resolveNucleusElectionGeography = ( nucleus: NucleusElectionGeographyInput, ): NucleusElectionGeography | null => { - const citiesFromNucleus = uniqueSortedCities(nucleus.cities.filter((city) => city.length > 0)) - const cities = - citiesFromNucleus.length > 0 - ? citiesFromNucleus - : uniqueSortedCities( - nucleus.regions.flatMap((region) => - isBahiaIdentityTerritory(region) ? citiesForTerritory(region) : [], - ), - ) - + const cities = resolveNucleusTerritoryCities(nucleus) if (cities.length === 0) return null const requestedZones = sortedUniqueZoneNumbers( diff --git a/src/utilities/nucleusIbgeVoterProfile.ts b/src/utilities/nucleusIbgeVoterProfile.ts new file mode 100644 index 00000000..b767a835 --- /dev/null +++ b/src/utilities/nucleusIbgeVoterProfile.ts @@ -0,0 +1,125 @@ +import { + demographicsForMunicipalityName, + type MunicipalityAgeBandKey, +} from '@/lib/bahiaMunicipalityDemographics' +import { + resolveNucleusTerritoryCities, + type NucleusTerritoryCitiesInput, +} from '@/utilities/nucleusTerritoryCities' + +export const IBGE_VOTER_PROFILE_LABEL = 'Perfil médio do território' + +export type ComputedVoterProfileViewModel = { + label: string + ageRange: string + localTraits: string + notes: string +} + +export type NucleusIbgeVoterProfileResult = + | { status: 'available'; profile: ComputedVoterProfileViewModel } + | { status: 'semPerfil' } + +const AGE_BAND_LABELS: Record = { + '0-17': '0 a 17 anos', + '18-29': '18 a 29 anos', + '30-59': '30 a 59 anos', + '60+': '60 anos ou mais', +} + +const percentFormatter = new Intl.NumberFormat('pt-BR', { + maximumFractionDigits: 1, + minimumFractionDigits: 0, +}) + +const integerFormatter = new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 0 }) + +const formatPercent = (value: number) => percentFormatter.format(value) + +const formatInteger = (value: number) => integerFormatter.format(value) + +const buildSummaryLine = ({ + dominantBand, + dominantSharePercent, + medianAge, + sexShareFemalePercent, +}: { + dominantBand: MunicipalityAgeBandKey + dominantSharePercent: number + medianAge: number | null + sexShareFemalePercent: number +}) => { + const medianText = + medianAge == null + ? 'idade mediana indisponível' + : `idade mediana de ${formatInteger(medianAge)} anos` + return `Predominam moradores na faixa de ${AGE_BAND_LABELS[dominantBand]} (${formatPercent(dominantSharePercent)}%). ${medianText.charAt(0).toUpperCase()}${medianText.slice(1)}; ${formatPercent(sexShareFemalePercent)}% mulheres.` +} + +export const getNucleusIbgeVoterProfile = ( + input: NucleusTerritoryCitiesInput, +): NucleusIbgeVoterProfileResult => { + const effectiveCities = resolveNucleusTerritoryCities(input) + if (effectiveCities.length === 0) return { status: 'semPerfil' } + + const ageBands: Record = { + '0-17': 0, + '18-29': 0, + '30-59': 0, + '60+': 0, + } + let population = 0 + let femalePopulation = 0 + let medianAgeWeighted = 0 + let medianAgeWeight = 0 + + for (const city of effectiveCities) { + const record = demographicsForMunicipalityName(city) + if (!record) continue + + population += record.population + femalePopulation += record.population * record.sexShareFemale + for (const band of Object.keys(ageBands) as MunicipalityAgeBandKey[]) { + ageBands[band] += record.ageBands[band] + } + if (record.medianAge != null) { + medianAgeWeighted += record.medianAge * record.population + medianAgeWeight += record.population + } + } + + if (population <= 0) return { status: 'semPerfil' } + + const ageBandShares = {} as Record + for (const band of Object.keys(ageBands) as MunicipalityAgeBandKey[]) { + ageBandShares[band] = (ageBands[band] / population) * 100 + } + + const dominantBand = (Object.keys(ageBandShares) as MunicipalityAgeBandKey[]).reduce( + (best, band) => (ageBandShares[band] > ageBandShares[best] ? band : best), + '0-17' as MunicipalityAgeBandKey, + ) + const sexShareFemalePercent = (femalePopulation / population) * 100 + const medianAge = medianAgeWeight > 0 ? medianAgeWeighted / medianAgeWeight : null + + const ageRange = (Object.keys(AGE_BAND_LABELS) as MunicipalityAgeBandKey[]) + .map((band) => `${AGE_BAND_LABELS[band]}: ${formatPercent(ageBandShares[band])}%`) + .join(' · ') + + const notes = `População residente estimada: ${formatInteger(population)} habitantes (Censo 2022, média ponderada de ${effectiveCities.length} município${effectiveCities.length === 1 ? '' : 's'}).` + + return { + status: 'available', + profile: { + label: IBGE_VOTER_PROFILE_LABEL, + ageRange, + localTraits: buildSummaryLine({ + dominantBand, + dominantSharePercent: ageBandShares[dominantBand], + medianAge, + sexShareFemalePercent, + }), + notes, + }, + } +} diff --git a/src/utilities/nucleusIntelligenceUi.ts b/src/utilities/nucleusIntelligenceUi.ts index 6165ba08..23d4244a 100644 --- a/src/utilities/nucleusIntelligenceUi.ts +++ b/src/utilities/nucleusIntelligenceUi.ts @@ -7,6 +7,49 @@ import { requiredRelationshipFormValue, } from '@/lib/formData' +export type VoterProfileWriteInput = { + label: string + ageRange?: string | null + incomeBand?: string | null + occupation?: string | null + localTraits?: string | null + notes?: string | null +} + +export type VoterProfileWriteShape = { + label: string + ageRange?: string + incomeBand?: string + occupation?: string + localTraits?: string + notes?: string +} + +export const voterProfileNullsToUndefined = ( + profile: VoterProfileWriteInput, +): VoterProfileWriteShape => ({ + label: profile.label, + ageRange: profile.ageRange ?? undefined, + incomeBand: profile.incomeBand ?? undefined, + occupation: profile.occupation ?? undefined, + localTraits: profile.localTraits ?? undefined, + notes: profile.notes ?? undefined, +}) + +export const normalizedVoterProfilesForWrite = ( + profiles: VoterProfileWriteInput[], +): VoterProfileWriteShape[] => + profiles + .map((profile) => ({ + label: profile.label.trim(), + ageRange: profile.ageRange?.trim() || undefined, + incomeBand: profile.incomeBand?.trim() || undefined, + occupation: profile.occupation?.trim() || undefined, + localTraits: profile.localTraits?.trim() || undefined, + notes: profile.notes?.trim() || undefined, + })) + .filter(({ label }) => Boolean(label)) + export const parseNucleusIntelligenceFormData = (formData: FormData): NucleusUpdateInput => { const hasTicketAlliance = ['partnerName', 'office', 'isCampaignPartner', 'allianceNotes'].some( (field) => formData.has(field), diff --git a/src/utilities/nucleusTerritoryCities.ts b/src/utilities/nucleusTerritoryCities.ts new file mode 100644 index 00000000..650f7987 --- /dev/null +++ b/src/utilities/nucleusTerritoryCities.ts @@ -0,0 +1,24 @@ +import { citiesForTerritory, isBahiaIdentityTerritory } from '@/lib/bahiaTerritories' + +export type NucleusTerritoryCitiesInput = { + cities: string[] + regions: string[] +} + +const uniqueSortedCities = (values: Iterable): string[] => + [...new Set(values)].sort((left, right) => left.localeCompare(right, 'pt-BR')) + +/** Cities explicitly set on the nucleus, or all cities from selected identity territories. */ +export const resolveNucleusTerritoryCities = ({ + cities, + regions, +}: NucleusTerritoryCitiesInput): string[] => { + const citiesFromNucleus = uniqueSortedCities([...cities].filter((city) => city.length > 0)) + if (citiesFromNucleus.length > 0) return citiesFromNucleus + + return uniqueSortedCities( + regions.flatMap((region) => + isBahiaIdentityTerritory(region) ? citiesForTerritory(region) : [], + ), + ) +} diff --git a/tests/fixtures/bahia-municipality-demographics.official.json b/tests/fixtures/bahia-municipality-demographics.official.json new file mode 100644 index 00000000..2ccd0b54 --- /dev/null +++ b/tests/fixtures/bahia-municipality-demographics.official.json @@ -0,0 +1,5010 @@ +{ + "municipalityCount": 417, + "assignments": [ + { + "code": "2900108", + "population": 7301, + "ageBands": { + "0-17": 1356, + "18-29": 967, + "30-59": 3015, + "60+": 1963 + }, + "sexShareFemale": 0.511574, + "medianAge": 43 + }, + { + "code": "2900207", + "population": 17639, + "ageBands": { + "0-17": 5376, + "18-29": 3442, + "30-59": 6579, + "60+": 2242 + }, + "sexShareFemale": 0.506605, + "medianAge": 30 + }, + { + "code": "2900306", + "population": 13795, + "ageBands": { + "0-17": 3667, + "18-29": 2485, + "30-59": 5356, + "60+": 2287 + }, + "sexShareFemale": 0.511997, + "medianAge": 34 + }, + { + "code": "2900355", + "population": 14201, + "ageBands": { + "0-17": 3316, + "18-29": 2711, + "30-59": 5877, + "60+": 2297 + }, + "sexShareFemale": 0.502218, + "medianAge": 34 + }, + { + "code": "2900405", + "population": 14497, + "ageBands": { + "0-17": 3485, + "18-29": 2511, + "30-59": 5845, + "60+": 2656 + }, + "sexShareFemale": 0.514313, + "medianAge": 36 + }, + { + "code": "2900504", + "population": 10604, + "ageBands": { + "0-17": 2243, + "18-29": 1992, + "30-59": 4376, + "60+": 1993 + }, + "sexShareFemale": 0.501603, + "medianAge": 36 + }, + { + "code": "2900603", + "population": 4447, + "ageBands": { + "0-17": 1076, + "18-29": 726, + "30-59": 1794, + "60+": 851 + }, + "sexShareFemale": 0.501462, + "medianAge": 37 + }, + { + "code": "2900702", + "population": 151055, + "ageBands": { + "0-17": 36332, + "18-29": 26395, + "30-59": 66436, + "60+": 21892 + }, + "sexShareFemale": 0.531727, + "medianAge": 35 + }, + { + "code": "2900801", + "population": 24530, + "ageBands": { + "0-17": 6863, + "18-29": 4117, + "30-59": 9751, + "60+": 3799 + }, + "sexShareFemale": 0.495516, + "medianAge": 33 + }, + { + "code": "2900900", + "population": 5218, + "ageBands": { + "0-17": 1198, + "18-29": 754, + "30-59": 2104, + "60+": 1162 + }, + "sexShareFemale": 0.510349, + "medianAge": 39 + }, + { + "code": "2901007", + "population": 36521, + "ageBands": { + "0-17": 8711, + "18-29": 7045, + "30-59": 14851, + "60+": 5914 + }, + "sexShareFemale": 0.51888, + "medianAge": 34 + }, + { + "code": "2901106", + "population": 24138, + "ageBands": { + "0-17": 5818, + "18-29": 4102, + "30-59": 10226, + "60+": 3992 + }, + "sexShareFemale": 0.53364, + "medianAge": 36 + }, + { + "code": "2901155", + "population": 15137, + "ageBands": { + "0-17": 4383, + "18-29": 2683, + "30-59": 5767, + "60+": 2304 + }, + "sexShareFemale": 0.495937, + "medianAge": 32 + }, + { + "code": "2901205", + "population": 25438, + "ageBands": { + "0-17": 5762, + "18-29": 4079, + "30-59": 10162, + "60+": 5435 + }, + "sexShareFemale": 0.49658, + "medianAge": 38 + }, + { + "code": "2901304", + "population": 13080, + "ageBands": { + "0-17": 3491, + "18-29": 2448, + "30-59": 5117, + "60+": 2024 + }, + "sexShareFemale": 0.498089, + "medianAge": 33 + }, + { + "code": "2901353", + "population": 15012, + "ageBands": { + "0-17": 3542, + "18-29": 2696, + "30-59": 6354, + "60+": 2420 + }, + "sexShareFemale": 0.502598, + "medianAge": 35 + }, + { + "code": "2901403", + "population": 13732, + "ageBands": { + "0-17": 3625, + "18-29": 2371, + "30-59": 5438, + "60+": 2298 + }, + "sexShareFemale": 0.485071, + "medianAge": 34 + }, + { + "code": "2901502", + "population": 11031, + "ageBands": { + "0-17": 2503, + "18-29": 2002, + "30-59": 4679, + "60+": 1847 + }, + "sexShareFemale": 0.510652, + "medianAge": 36 + }, + { + "code": "2901601", + "population": 14206, + "ageBands": { + "0-17": 2896, + "18-29": 2648, + "30-59": 6059, + "60+": 2603 + }, + "sexShareFemale": 0.498381, + "medianAge": 36 + }, + { + "code": "2901700", + "population": 11146, + "ageBands": { + "0-17": 3011, + "18-29": 2091, + "30-59": 4351, + "60+": 1693 + }, + "sexShareFemale": 0.514265, + "medianAge": 32 + }, + { + "code": "2901809", + "population": 10862, + "ageBands": { + "0-17": 2779, + "18-29": 1916, + "30-59": 4451, + "60+": 1716 + }, + "sexShareFemale": 0.499632, + "medianAge": 34 + }, + { + "code": "2901908", + "population": 15922, + "ageBands": { + "0-17": 4037, + "18-29": 2834, + "30-59": 6279, + "60+": 2772 + }, + "sexShareFemale": 0.496608, + "medianAge": 35 + }, + { + "code": "2901957", + "population": 6913, + "ageBands": { + "0-17": 1775, + "18-29": 1195, + "30-59": 2661, + "60+": 1282 + }, + "sexShareFemale": 0.496167, + "medianAge": 35 + }, + { + "code": "2902005", + "population": 13936, + "ageBands": { + "0-17": 3211, + "18-29": 2304, + "30-59": 5656, + "60+": 2765 + }, + "sexShareFemale": 0.489165, + "medianAge": 37 + }, + { + "code": "2902054", + "population": 11557, + "ageBands": { + "0-17": 3203, + "18-29": 2310, + "30-59": 4642, + "60+": 1402 + }, + "sexShareFemale": 0.516916, + "medianAge": 31 + }, + { + "code": "2902104", + "population": 48294, + "ageBands": { + "0-17": 13010, + "18-29": 9431, + "30-59": 18633, + "60+": 7220 + }, + "sexShareFemale": 0.501097, + "medianAge": 32 + }, + { + "code": "2902203", + "population": 9833, + "ageBands": { + "0-17": 2429, + "18-29": 1786, + "30-59": 4131, + "60+": 1487 + }, + "sexShareFemale": 0.515916, + "medianAge": 35 + }, + { + "code": "2902252", + "population": 10191, + "ageBands": { + "0-17": 2737, + "18-29": 1869, + "30-59": 3985, + "60+": 1600 + }, + "sexShareFemale": 0.472181, + "medianAge": 33 + }, + { + "code": "2902302", + "population": 8677, + "ageBands": { + "0-17": 2261, + "18-29": 1602, + "30-59": 3639, + "60+": 1175 + }, + "sexShareFemale": 0.495448, + "medianAge": 33 + }, + { + "code": "2902401", + "population": 11179, + "ageBands": { + "0-17": 3258, + "18-29": 1881, + "30-59": 4287, + "60+": 1753 + }, + "sexShareFemale": 0.507738, + "medianAge": 33 + }, + { + "code": "2902500", + "population": 13614, + "ageBands": { + "0-17": 3668, + "18-29": 2449, + "30-59": 5297, + "60+": 2200 + }, + "sexShareFemale": 0.485897, + "medianAge": 33 + }, + { + "code": "2902609", + "population": 18220, + "ageBands": { + "0-17": 4500, + "18-29": 3192, + "30-59": 7054, + "60+": 3474 + }, + "sexShareFemale": 0.503238, + "medianAge": 36 + }, + { + "code": "2902658", + "population": 11958, + "ageBands": { + "0-17": 3360, + "18-29": 2096, + "30-59": 4583, + "60+": 1919 + }, + "sexShareFemale": 0.496153, + "medianAge": 33 + }, + { + "code": "2902708", + "population": 51092, + "ageBands": { + "0-17": 16288, + "18-29": 9907, + "30-59": 18520, + "60+": 6377 + }, + "sexShareFemale": 0.49129, + "medianAge": 29 + }, + { + "code": "2902807", + "population": 26026, + "ageBands": { + "0-17": 6778, + "18-29": 5221, + "30-59": 10605, + "60+": 3422 + }, + "sexShareFemale": 0.495504, + "medianAge": 32 + }, + { + "code": "2902906", + "population": 36539, + "ageBands": { + "0-17": 10124, + "18-29": 6696, + "30-59": 14500, + "60+": 5219 + }, + "sexShareFemale": 0.50754, + "medianAge": 32 + }, + { + "code": "2903003", + "population": 13836, + "ageBands": { + "0-17": 3320, + "18-29": 2314, + "30-59": 5383, + "60+": 2819 + }, + "sexShareFemale": 0.495302, + "medianAge": 37 + }, + { + "code": "2903102", + "population": 5775, + "ageBands": { + "0-17": 1469, + "18-29": 919, + "30-59": 2377, + "60+": 1010 + }, + "sexShareFemale": 0.477576, + "medianAge": 36 + }, + { + "code": "2903201", + "population": 159734, + "ageBands": { + "0-17": 43478, + "18-29": 32658, + "30-59": 67612, + "60+": 15986 + }, + "sexShareFemale": 0.510943, + "medianAge": 31 + }, + { + "code": "2903235", + "population": 13453, + "ageBands": { + "0-17": 3375, + "18-29": 2431, + "30-59": 5434, + "60+": 2213 + }, + "sexShareFemale": 0.494908, + "medianAge": 35 + }, + { + "code": "2903276", + "population": 15203, + "ageBands": { + "0-17": 4005, + "18-29": 2742, + "30-59": 6371, + "60+": 2085 + }, + "sexShareFemale": 0.505558, + "medianAge": 33 + }, + { + "code": "2903300", + "population": 5833, + "ageBands": { + "0-17": 1354, + "18-29": 955, + "30-59": 2420, + "60+": 1104 + }, + "sexShareFemale": 0.499914, + "medianAge": 37 + }, + { + "code": "2903409", + "population": 20121, + "ageBands": { + "0-17": 5662, + "18-29": 3459, + "30-59": 7881, + "60+": 3119 + }, + "sexShareFemale": 0.496446, + "medianAge": 33 + }, + { + "code": "2903508", + "population": 18412, + "ageBands": { + "0-17": 4472, + "18-29": 3144, + "30-59": 7193, + "60+": 3603 + }, + "sexShareFemale": 0.501195, + "medianAge": 36 + }, + { + "code": "2903607", + "population": 15146, + "ageBands": { + "0-17": 3852, + "18-29": 2663, + "30-59": 6108, + "60+": 2523 + }, + "sexShareFemale": 0.497425, + "medianAge": 34 + }, + { + "code": "2903706", + "population": 13690, + "ageBands": { + "0-17": 3430, + "18-29": 2343, + "30-59": 5196, + "60+": 2721 + }, + "sexShareFemale": 0.501899, + "medianAge": 35 + }, + { + "code": "2903805", + "population": 16873, + "ageBands": { + "0-17": 4650, + "18-29": 2774, + "30-59": 6506, + "60+": 2943 + }, + "sexShareFemale": 0.492977, + "medianAge": 34 + }, + { + "code": "2903904", + "population": 65550, + "ageBands": { + "0-17": 18854, + "18-29": 12667, + "30-59": 25605, + "60+": 8424 + }, + "sexShareFemale": 0.50926, + "medianAge": 31 + }, + { + "code": "2903953", + "population": 9730, + "ageBands": { + "0-17": 2415, + "18-29": 1810, + "30-59": 3754, + "60+": 1751 + }, + "sexShareFemale": 0.483864, + "medianAge": 34 + }, + { + "code": "2904001", + "population": 13622, + "ageBands": { + "0-17": 3496, + "18-29": 2319, + "30-59": 5329, + "60+": 2478 + }, + "sexShareFemale": 0.510277, + "medianAge": 35 + }, + { + "code": "2904050", + "population": 15844, + "ageBands": { + "0-17": 4686, + "18-29": 3410, + "30-59": 6066, + "60+": 1682 + }, + "sexShareFemale": 0.500063, + "medianAge": 29 + }, + { + "code": "2904100", + "population": 19322, + "ageBands": { + "0-17": 4699, + "18-29": 3340, + "30-59": 7484, + "60+": 3799 + }, + "sexShareFemale": 0.508074, + "medianAge": 36 + }, + { + "code": "2904209", + "population": 11024, + "ageBands": { + "0-17": 2518, + "18-29": 2083, + "30-59": 4374, + "60+": 2049 + }, + "sexShareFemale": 0.497823, + "medianAge": 35 + }, + { + "code": "2904308", + "population": 12943, + "ageBands": { + "0-17": 3268, + "18-29": 2213, + "30-59": 5124, + "60+": 2338 + }, + "sexShareFemale": 0.507997, + "medianAge": 35 + }, + { + "code": "2904407", + "population": 9108, + "ageBands": { + "0-17": 2222, + "18-29": 1300, + "30-59": 3758, + "60+": 1828 + }, + "sexShareFemale": 0.485617, + "medianAge": 38 + }, + { + "code": "2904506", + "population": 11765, + "ageBands": { + "0-17": 2906, + "18-29": 1833, + "30-59": 4489, + "60+": 2537 + }, + "sexShareFemale": 0.487123, + "medianAge": 37 + }, + { + "code": "2904605", + "population": 70510, + "ageBands": { + "0-17": 16094, + "18-29": 12337, + "30-59": 30638, + "60+": 11441 + }, + "sexShareFemale": 0.512523, + "medianAge": 36 + }, + { + "code": "2904704", + "population": 14804, + "ageBands": { + "0-17": 3609, + "18-29": 2298, + "30-59": 5830, + "60+": 3067 + }, + "sexShareFemale": 0.512497, + "medianAge": 37 + }, + { + "code": "2904753", + "population": 19589, + "ageBands": { + "0-17": 5733, + "18-29": 3806, + "30-59": 7509, + "60+": 2541 + }, + "sexShareFemale": 0.479249, + "medianAge": 30 + }, + { + "code": "2904803", + "population": 6205, + "ageBands": { + "0-17": 1435, + "18-29": 1001, + "30-59": 2627, + "60+": 1142 + }, + "sexShareFemale": 0.486382, + "medianAge": 37 + }, + { + "code": "2904852", + "population": 16559, + "ageBands": { + "0-17": 4683, + "18-29": 3292, + "30-59": 6318, + "60+": 2266 + }, + "sexShareFemale": 0.517362, + "medianAge": 31 + }, + { + "code": "2904902", + "population": 29250, + "ageBands": { + "0-17": 7030, + "18-29": 5402, + "30-59": 12121, + "60+": 4697 + }, + "sexShareFemale": 0.521846, + "medianAge": 35 + }, + { + "code": "2905008", + "population": 22462, + "ageBands": { + "0-17": 5044, + "18-29": 3987, + "30-59": 9293, + "60+": 4138 + }, + "sexShareFemale": 0.506411, + "medianAge": 36 + }, + { + "code": "2905107", + "population": 10384, + "ageBands": { + "0-17": 2806, + "18-29": 1890, + "30-59": 3948, + "60+": 1740 + }, + "sexShareFemale": 0.503948, + "medianAge": 33 + }, + { + "code": "2905156", + "population": 11266, + "ageBands": { + "0-17": 2663, + "18-29": 1921, + "30-59": 4556, + "60+": 2126 + }, + "sexShareFemale": 0.488638, + "medianAge": 36 + }, + { + "code": "2905206", + "population": 52012, + "ageBands": { + "0-17": 12293, + "18-29": 9487, + "30-59": 21605, + "60+": 8627 + }, + "sexShareFemale": 0.508979, + "medianAge": 34 + }, + { + "code": "2905305", + "population": 17466, + "ageBands": { + "0-17": 4890, + "18-29": 3328, + "30-59": 6865, + "60+": 2383 + }, + "sexShareFemale": 0.494733, + "medianAge": 32 + }, + { + "code": "2905404", + "population": 17761, + "ageBands": { + "0-17": 4887, + "18-29": 3701, + "30-59": 7453, + "60+": 1720 + }, + "sexShareFemale": 0.510669, + "medianAge": 30 + }, + { + "code": "2905503", + "population": 13080, + "ageBands": { + "0-17": 3634, + "18-29": 2411, + "30-59": 5006, + "60+": 2029 + }, + "sexShareFemale": 0.50604, + "medianAge": 32 + }, + { + "code": "2905602", + "population": 22579, + "ageBands": { + "0-17": 6159, + "18-29": 3892, + "30-59": 8514, + "60+": 4014 + }, + "sexShareFemale": 0.513265, + "medianAge": 34 + }, + { + "code": "2905701", + "population": 300372, + "ageBands": { + "0-17": 77870, + "18-29": 53220, + "30-59": 136559, + "60+": 32723 + }, + "sexShareFemale": 0.519696, + "medianAge": 34 + }, + { + "code": "2905800", + "population": 30469, + "ageBands": { + "0-17": 9565, + "18-29": 5474, + "30-59": 11196, + "60+": 4234 + }, + "sexShareFemale": 0.510289, + "medianAge": 30 + }, + { + "code": "2905909", + "population": 30671, + "ageBands": { + "0-17": 9034, + "18-29": 5266, + "30-59": 11674, + "60+": 4697 + }, + "sexShareFemale": 0.493952, + "medianAge": 32 + }, + { + "code": "2906006", + "population": 71377, + "ageBands": { + "0-17": 19162, + "18-29": 13848, + "30-59": 28262, + "60+": 10105 + }, + "sexShareFemale": 0.502711, + "medianAge": 32 + }, + { + "code": "2906105", + "population": 10225, + "ageBands": { + "0-17": 2785, + "18-29": 1866, + "30-59": 3882, + "60+": 1692 + }, + "sexShareFemale": 0.481174, + "medianAge": 33 + }, + { + "code": "2906204", + "population": 24206, + "ageBands": { + "0-17": 6536, + "18-29": 4573, + "30-59": 9467, + "60+": 3630 + }, + "sexShareFemale": 0.501983, + "medianAge": 33 + }, + { + "code": "2906303", + "population": 32683, + "ageBands": { + "0-17": 8439, + "18-29": 5498, + "30-59": 13139, + "60+": 5607 + }, + "sexShareFemale": 0.500382, + "medianAge": 35 + }, + { + "code": "2906402", + "population": 7772, + "ageBands": { + "0-17": 1763, + "18-29": 1350, + "30-59": 3112, + "60+": 1547 + }, + "sexShareFemale": 0.502316, + "medianAge": 38 + }, + { + "code": "2906501", + "population": 72382, + "ageBands": { + "0-17": 17722, + "18-29": 12420, + "30-59": 32220, + "60+": 10020 + }, + "sexShareFemale": 0.531334, + "medianAge": 35 + }, + { + "code": "2906600", + "population": 13016, + "ageBands": { + "0-17": 2868, + "18-29": 2002, + "30-59": 5390, + "60+": 2756 + }, + "sexShareFemale": 0.497849, + "medianAge": 38 + }, + { + "code": "2906709", + "population": 25247, + "ageBands": { + "0-17": 6386, + "18-29": 4170, + "30-59": 10177, + "60+": 4514 + }, + "sexShareFemale": 0.502515, + "medianAge": 36 + }, + { + "code": "2906808", + "population": 37439, + "ageBands": { + "0-17": 9773, + "18-29": 7548, + "30-59": 14553, + "60+": 5565 + }, + "sexShareFemale": 0.497182, + "medianAge": 32 + }, + { + "code": "2906824", + "population": 16105, + "ageBands": { + "0-17": 4539, + "18-29": 2813, + "30-59": 6332, + "60+": 2421 + }, + "sexShareFemale": 0.494753, + "medianAge": 33 + }, + { + "code": "2906857", + "population": 10744, + "ageBands": { + "0-17": 2284, + "18-29": 1658, + "30-59": 4455, + "60+": 2347 + }, + "sexShareFemale": 0.502699, + "medianAge": 39 + }, + { + "code": "2906873", + "population": 33235, + "ageBands": { + "0-17": 8822, + "18-29": 6196, + "30-59": 13457, + "60+": 4760 + }, + "sexShareFemale": 0.513886, + "medianAge": 33 + }, + { + "code": "2906899", + "population": 9940, + "ageBands": { + "0-17": 2229, + "18-29": 1553, + "30-59": 3837, + "60+": 2321 + }, + "sexShareFemale": 0.492656, + "medianAge": 39 + }, + { + "code": "2906907", + "population": 20580, + "ageBands": { + "0-17": 5638, + "18-29": 3633, + "30-59": 8060, + "60+": 3249 + }, + "sexShareFemale": 0.501555, + "medianAge": 33 + }, + { + "code": "2907004", + "population": 8365, + "ageBands": { + "0-17": 2457, + "18-29": 1700, + "30-59": 3231, + "60+": 977 + }, + "sexShareFemale": 0.516916, + "medianAge": 30 + }, + { + "code": "2907103", + "population": 28869, + "ageBands": { + "0-17": 8498, + "18-29": 5315, + "30-59": 11113, + "60+": 3943 + }, + "sexShareFemale": 0.489764, + "medianAge": 31 + }, + { + "code": "2907202", + "population": 72086, + "ageBands": { + "0-17": 20699, + "18-29": 13940, + "30-59": 27931, + "60+": 9516 + }, + "sexShareFemale": 0.488652, + "medianAge": 31 + }, + { + "code": "2907301", + "population": 24712, + "ageBands": { + "0-17": 5938, + "18-29": 4382, + "30-59": 10385, + "60+": 4007 + }, + "sexShareFemale": 0.52602, + "medianAge": 35 + }, + { + "code": "2907400", + "population": 3434, + "ageBands": { + "0-17": 941, + "18-29": 491, + "30-59": 1359, + "60+": 643 + }, + "sexShareFemale": 0.481072, + "medianAge": 36 + }, + { + "code": "2907509", + "population": 48148, + "ageBands": { + "0-17": 11182, + "18-29": 7790, + "30-59": 21652, + "60+": 7524 + }, + "sexShareFemale": 0.532836, + "medianAge": 37 + }, + { + "code": "2907558", + "population": 8841, + "ageBands": { + "0-17": 1939, + "18-29": 1513, + "30-59": 3584, + "60+": 1805 + }, + "sexShareFemale": 0.49146, + "medianAge": 37 + }, + { + "code": "2907608", + "population": 16348, + "ageBands": { + "0-17": 4218, + "18-29": 2710, + "30-59": 6514, + "60+": 2906 + }, + "sexShareFemale": 0.493883, + "medianAge": 35 + }, + { + "code": "2907707", + "population": 10579, + "ageBands": { + "0-17": 2990, + "18-29": 2045, + "30-59": 3957, + "60+": 1587 + }, + "sexShareFemale": 0.494092, + "medianAge": 31 + }, + { + "code": "2907806", + "population": 30907, + "ageBands": { + "0-17": 6712, + "18-29": 5412, + "30-59": 12931, + "60+": 5852 + }, + "sexShareFemale": 0.506973, + "medianAge": 37 + }, + { + "code": "2907905", + "population": 17230, + "ageBands": { + "0-17": 4762, + "18-29": 3195, + "30-59": 6861, + "60+": 2412 + }, + "sexShareFemale": 0.512362, + "medianAge": 32 + }, + { + "code": "2908002", + "population": 17333, + "ageBands": { + "0-17": 4061, + "18-29": 2657, + "30-59": 6743, + "60+": 3872 + }, + "sexShareFemale": 0.523337, + "medianAge": 38 + }, + { + "code": "2908101", + "population": 19151, + "ageBands": { + "0-17": 4884, + "18-29": 3645, + "30-59": 7375, + "60+": 3247 + }, + "sexShareFemale": 0.489269, + "medianAge": 33 + }, + { + "code": "2908200", + "population": 20800, + "ageBands": { + "0-17": 5174, + "18-29": 3976, + "30-59": 8683, + "60+": 2967 + }, + "sexShareFemale": 0.525817, + "medianAge": 34 + }, + { + "code": "2908309", + "population": 15794, + "ageBands": { + "0-17": 3335, + "18-29": 2616, + "30-59": 6916, + "60+": 2927 + }, + "sexShareFemale": 0.52273, + "medianAge": 38 + }, + { + "code": "2908408", + "population": 67825, + "ageBands": { + "0-17": 17610, + "18-29": 12242, + "30-59": 27937, + "60+": 10036 + }, + "sexShareFemale": 0.514943, + "medianAge": 34 + }, + { + "code": "2908507", + "population": 35308, + "ageBands": { + "0-17": 8455, + "18-29": 6412, + "30-59": 15506, + "60+": 4935 + }, + "sexShareFemale": 0.524584, + "medianAge": 35 + }, + { + "code": "2908606", + "population": 23654, + "ageBands": { + "0-17": 6892, + "18-29": 4956, + "30-59": 8755, + "60+": 3051 + }, + "sexShareFemale": 0.504777, + "medianAge": 29 + }, + { + "code": "2908705", + "population": 17053, + "ageBands": { + "0-17": 3817, + "18-29": 2609, + "30-59": 6976, + "60+": 3651 + }, + "sexShareFemale": 0.507242, + "medianAge": 39 + }, + { + "code": "2908804", + "population": 4333, + "ageBands": { + "0-17": 1005, + "18-29": 729, + "30-59": 1721, + "60+": 878 + }, + "sexShareFemale": 0.494115, + "medianAge": 36 + }, + { + "code": "2908903", + "population": 26692, + "ageBands": { + "0-17": 6380, + "18-29": 5015, + "30-59": 11202, + "60+": 4095 + }, + "sexShareFemale": 0.519069, + "medianAge": 34 + }, + { + "code": "2909000", + "population": 7546, + "ageBands": { + "0-17": 1618, + "18-29": 1136, + "30-59": 3129, + "60+": 1663 + }, + "sexShareFemale": 0.509674, + "medianAge": 40 + }, + { + "code": "2909109", + "population": 13990, + "ageBands": { + "0-17": 3355, + "18-29": 2194, + "30-59": 5698, + "60+": 2743 + }, + "sexShareFemale": 0.488277, + "medianAge": 37 + }, + { + "code": "2909208", + "population": 17056, + "ageBands": { + "0-17": 4364, + "18-29": 3443, + "30-59": 6774, + "60+": 2475 + }, + "sexShareFemale": 0.487394, + "medianAge": 32 + }, + { + "code": "2909307", + "population": 32457, + "ageBands": { + "0-17": 8374, + "18-29": 5784, + "30-59": 13168, + "60+": 5131 + }, + "sexShareFemale": 0.498629, + "medianAge": 34 + }, + { + "code": "2909406", + "population": 13063, + "ageBands": { + "0-17": 3557, + "18-29": 2272, + "30-59": 5106, + "60+": 2128 + }, + "sexShareFemale": 0.486948, + "medianAge": 33 + }, + { + "code": "2909505", + "population": 4415, + "ageBands": { + "0-17": 1085, + "18-29": 671, + "30-59": 1832, + "60+": 827 + }, + "sexShareFemale": 0.50419, + "medianAge": 37 + }, + { + "code": "2909604", + "population": 19729, + "ageBands": { + "0-17": 5124, + "18-29": 3476, + "30-59": 7891, + "60+": 3238 + }, + "sexShareFemale": 0.498657, + "medianAge": 35 + }, + { + "code": "2909703", + "population": 13993, + "ageBands": { + "0-17": 3520, + "18-29": 2538, + "30-59": 5667, + "60+": 2268 + }, + "sexShareFemale": 0.486815, + "medianAge": 34 + }, + { + "code": "2909802", + "population": 60348, + "ageBands": { + "0-17": 13723, + "18-29": 10492, + "30-59": 26851, + "60+": 9282 + }, + "sexShareFemale": 0.534135, + "medianAge": 36 + }, + { + "code": "2909901", + "population": 34180, + "ageBands": { + "0-17": 9974, + "18-29": 6420, + "30-59": 13254, + "60+": 4532 + }, + "sexShareFemale": 0.489585, + "medianAge": 31 + }, + { + "code": "2910008", + "population": 10820, + "ageBands": { + "0-17": 2976, + "18-29": 1772, + "30-59": 4142, + "60+": 1930 + }, + "sexShareFemale": 0.481701, + "medianAge": 34 + }, + { + "code": "2910057", + "population": 71485, + "ageBands": { + "0-17": 19227, + "18-29": 12666, + "30-59": 31098, + "60+": 8494 + }, + "sexShareFemale": 0.519326, + "medianAge": 33 + }, + { + "code": "2910107", + "population": 11884, + "ageBands": { + "0-17": 2585, + "18-29": 2061, + "30-59": 5170, + "60+": 2068 + }, + "sexShareFemale": 0.48965, + "medianAge": 36 + }, + { + "code": "2910206", + "population": 4407, + "ageBands": { + "0-17": 997, + "18-29": 741, + "30-59": 1912, + "60+": 757 + }, + "sexShareFemale": 0.51577, + "medianAge": 37 + }, + { + "code": "2910305", + "population": 7808, + "ageBands": { + "0-17": 1724, + "18-29": 1132, + "30-59": 3310, + "60+": 1642 + }, + "sexShareFemale": 0.504355, + "medianAge": 39 + }, + { + "code": "2910404", + "population": 19107, + "ageBands": { + "0-17": 5136, + "18-29": 3309, + "30-59": 7460, + "60+": 3202 + }, + "sexShareFemale": 0.483906, + "medianAge": 34 + }, + { + "code": "2910503", + "population": 38098, + "ageBands": { + "0-17": 10048, + "18-29": 7282, + "30-59": 15658, + "60+": 5110 + }, + "sexShareFemale": 0.519922, + "medianAge": 33 + }, + { + "code": "2910602", + "population": 32554, + "ageBands": { + "0-17": 9194, + "18-29": 6571, + "30-59": 12574, + "60+": 4215 + }, + "sexShareFemale": 0.513792, + "medianAge": 31 + }, + { + "code": "2910701", + "population": 61456, + "ageBands": { + "0-17": 15452, + "18-29": 10987, + "30-59": 24817, + "60+": 10200 + }, + "sexShareFemale": 0.501188, + "medianAge": 34 + }, + { + "code": "2910727", + "population": 113710, + "ageBands": { + "0-17": 30851, + "18-29": 20849, + "30-59": 47401, + "60+": 14609 + }, + "sexShareFemale": 0.515161, + "medianAge": 33 + }, + { + "code": "2910750", + "population": 17895, + "ageBands": { + "0-17": 3976, + "18-29": 3285, + "30-59": 7564, + "60+": 3070 + }, + "sexShareFemale": 0.499357, + "medianAge": 36 + }, + { + "code": "2910776", + "population": 5631, + "ageBands": { + "0-17": 1362, + "18-29": 876, + "30-59": 2345, + "60+": 1048 + }, + "sexShareFemale": 0.487658, + "medianAge": 37 + }, + { + "code": "2910800", + "population": 616272, + "ageBands": { + "0-17": 150536, + "18-29": 116997, + "30-59": 269294, + "60+": 79445 + }, + "sexShareFemale": 0.534266, + "medianAge": 34 + }, + { + "code": "2910859", + "population": 17897, + "ageBands": { + "0-17": 4622, + "18-29": 3346, + "30-59": 7229, + "60+": 2700 + }, + "sexShareFemale": 0.501034, + "medianAge": 33 + }, + { + "code": "2910909", + "population": 4873, + "ageBands": { + "0-17": 1229, + "18-29": 819, + "30-59": 1947, + "60+": 878 + }, + "sexShareFemale": 0.496204, + "medianAge": 35 + }, + { + "code": "2911006", + "population": 11059, + "ageBands": { + "0-17": 2515, + "18-29": 1765, + "30-59": 4588, + "60+": 2191 + }, + "sexShareFemale": 0.491184, + "medianAge": 37 + }, + { + "code": "2911105", + "population": 25899, + "ageBands": { + "0-17": 7730, + "18-29": 5289, + "30-59": 9757, + "60+": 3123 + }, + "sexShareFemale": 0.494421, + "medianAge": 29 + }, + { + "code": "2911204", + "population": 32178, + "ageBands": { + "0-17": 8512, + "18-29": 5687, + "30-59": 13469, + "60+": 4510 + }, + "sexShareFemale": 0.511095, + "medianAge": 34 + }, + { + "code": "2911253", + "population": 4360, + "ageBands": { + "0-17": 960, + "18-29": 693, + "30-59": 1774, + "60+": 933 + }, + "sexShareFemale": 0.486697, + "medianAge": 38 + }, + { + "code": "2911303", + "population": 10884, + "ageBands": { + "0-17": 2709, + "18-29": 1947, + "30-59": 4294, + "60+": 1934 + }, + "sexShareFemale": 0.486953, + "medianAge": 35 + }, + { + "code": "2911402", + "population": 15524, + "ageBands": { + "0-17": 4230, + "18-29": 2576, + "30-59": 6425, + "60+": 2293 + }, + "sexShareFemale": 0.509469, + "medianAge": 34 + }, + { + "code": "2911501", + "population": 5555, + "ageBands": { + "0-17": 1459, + "18-29": 922, + "30-59": 2150, + "60+": 1024 + }, + "sexShareFemale": 0.50135, + "medianAge": 35 + }, + { + "code": "2911600", + "population": 20605, + "ageBands": { + "0-17": 5149, + "18-29": 3563, + "30-59": 8686, + "60+": 3207 + }, + "sexShareFemale": 0.53123, + "medianAge": 35 + }, + { + "code": "2911659", + "population": 8050, + "ageBands": { + "0-17": 1677, + "18-29": 1320, + "30-59": 3200, + "60+": 1853 + }, + "sexShareFemale": 0.50087, + "medianAge": 40 + }, + { + "code": "2911709", + "population": 87817, + "ageBands": { + "0-17": 20825, + "18-29": 15921, + "30-59": 37560, + "60+": 13511 + }, + "sexShareFemale": 0.515117, + "medianAge": 35 + }, + { + "code": "2911808", + "population": 19049, + "ageBands": { + "0-17": 4875, + "18-29": 2935, + "30-59": 7625, + "60+": 3614 + }, + "sexShareFemale": 0.485747, + "medianAge": 36 + }, + { + "code": "2911857", + "population": 12309, + "ageBands": { + "0-17": 2676, + "18-29": 2406, + "30-59": 5075, + "60+": 2152 + }, + "sexShareFemale": 0.497928, + "medianAge": 36 + }, + { + "code": "2911907", + "population": 24607, + "ageBands": { + "0-17": 6448, + "18-29": 4253, + "30-59": 9625, + "60+": 4281 + }, + "sexShareFemale": 0.516032, + "medianAge": 35 + }, + { + "code": "2912004", + "population": 10429, + "ageBands": { + "0-17": 2049, + "18-29": 1671, + "30-59": 4426, + "60+": 2283 + }, + "sexShareFemale": 0.500431, + "medianAge": 40 + }, + { + "code": "2912103", + "population": 21665, + "ageBands": { + "0-17": 5065, + "18-29": 3617, + "30-59": 8526, + "60+": 4457 + }, + "sexShareFemale": 0.518024, + "medianAge": 37 + }, + { + "code": "2912202", + "population": 20785, + "ageBands": { + "0-17": 6169, + "18-29": 4327, + "30-59": 8277, + "60+": 2012 + }, + "sexShareFemale": 0.498773, + "medianAge": 29 + }, + { + "code": "2912301", + "population": 13934, + "ageBands": { + "0-17": 3303, + "18-29": 2226, + "30-59": 5687, + "60+": 2718 + }, + "sexShareFemale": 0.502655, + "medianAge": 37 + }, + { + "code": "2912400", + "population": 16603, + "ageBands": { + "0-17": 4196, + "18-29": 2702, + "30-59": 6818, + "60+": 2887 + }, + "sexShareFemale": 0.494429, + "medianAge": 36 + }, + { + "code": "2912509", + "population": 13863, + "ageBands": { + "0-17": 3118, + "18-29": 2590, + "30-59": 5543, + "60+": 2612 + }, + "sexShareFemale": 0.502489, + "medianAge": 36 + }, + { + "code": "2912608", + "population": 3725, + "ageBands": { + "0-17": 990, + "18-29": 631, + "30-59": 1464, + "60+": 640 + }, + "sexShareFemale": 0.488322, + "medianAge": 35 + }, + { + "code": "2912707", + "population": 25344, + "ageBands": { + "0-17": 7211, + "18-29": 4981, + "30-59": 9411, + "60+": 3741 + }, + "sexShareFemale": 0.483546, + "medianAge": 31 + }, + { + "code": "2912806", + "population": 8896, + "ageBands": { + "0-17": 2316, + "18-29": 1518, + "30-59": 3565, + "60+": 1497 + }, + "sexShareFemale": 0.497639, + "medianAge": 34 + }, + { + "code": "2912905", + "population": 18792, + "ageBands": { + "0-17": 4850, + "18-29": 2874, + "30-59": 7602, + "60+": 3466 + }, + "sexShareFemale": 0.513995, + "medianAge": 36 + }, + { + "code": "2913002", + "population": 14637, + "ageBands": { + "0-17": 3187, + "18-29": 2525, + "30-59": 5850, + "60+": 3075 + }, + "sexShareFemale": 0.501674, + "medianAge": 37 + }, + { + "code": "2913101", + "population": 16969, + "ageBands": { + "0-17": 4186, + "18-29": 2790, + "30-59": 6968, + "60+": 3025 + }, + "sexShareFemale": 0.497613, + "medianAge": 36 + }, + { + "code": "2913200", + "population": 26309, + "ageBands": { + "0-17": 7256, + "18-29": 4823, + "30-59": 10449, + "60+": 3781 + }, + "sexShareFemale": 0.510016, + "medianAge": 32 + }, + { + "code": "2913309", + "population": 6190, + "ageBands": { + "0-17": 1444, + "18-29": 1079, + "30-59": 2521, + "60+": 1146 + }, + "sexShareFemale": 0.513409, + "medianAge": 36 + }, + { + "code": "2913408", + "population": 15527, + "ageBands": { + "0-17": 3427, + "18-29": 2795, + "30-59": 6460, + "60+": 2845 + }, + "sexShareFemale": 0.507825, + "medianAge": 36 + }, + { + "code": "2913457", + "population": 13151, + "ageBands": { + "0-17": 3627, + "18-29": 2525, + "30-59": 5266, + "60+": 1733 + }, + "sexShareFemale": 0.48521, + "medianAge": 32 + }, + { + "code": "2913507", + "population": 21491, + "ageBands": { + "0-17": 5467, + "18-29": 3426, + "30-59": 8608, + "60+": 3990 + }, + "sexShareFemale": 0.493788, + "medianAge": 36 + }, + { + "code": "2913606", + "population": 178649, + "ageBands": { + "0-17": 42464, + "18-29": 30693, + "30-59": 76088, + "60+": 29404 + }, + "sexShareFemale": 0.521173, + "medianAge": 36 + }, + { + "code": "2913705", + "population": 33790, + "ageBands": { + "0-17": 8968, + "18-29": 6430, + "30-59": 13416, + "60+": 4976 + }, + "sexShareFemale": 0.511246, + "medianAge": 33 + }, + { + "code": "2913804", + "population": 13709, + "ageBands": { + "0-17": 3340, + "18-29": 2742, + "30-59": 5271, + "60+": 2356 + }, + "sexShareFemale": 0.513458, + "medianAge": 34 + }, + { + "code": "2913903", + "population": 40706, + "ageBands": { + "0-17": 10013, + "18-29": 6629, + "30-59": 16610, + "60+": 7454 + }, + "sexShareFemale": 0.518646, + "medianAge": 36 + }, + { + "code": "2914000", + "population": 56876, + "ageBands": { + "0-17": 14090, + "18-29": 10098, + "30-59": 22601, + "60+": 10087 + }, + "sexShareFemale": 0.517002, + "medianAge": 35 + }, + { + "code": "2914109", + "population": 9935, + "ageBands": { + "0-17": 2275, + "18-29": 1678, + "30-59": 4006, + "60+": 1976 + }, + "sexShareFemale": 0.501963, + "medianAge": 37 + }, + { + "code": "2914208", + "population": 6101, + "ageBands": { + "0-17": 1547, + "18-29": 1003, + "30-59": 2425, + "60+": 1126 + }, + "sexShareFemale": 0.506966, + "medianAge": 36 + }, + { + "code": "2914307", + "population": 10752, + "ageBands": { + "0-17": 2585, + "18-29": 2078, + "30-59": 4249, + "60+": 1840 + }, + "sexShareFemale": 0.489676, + "medianAge": 34 + }, + { + "code": "2914406", + "population": 23879, + "ageBands": { + "0-17": 6517, + "18-29": 4791, + "30-59": 9188, + "60+": 3383 + }, + "sexShareFemale": 0.505046, + "medianAge": 31 + }, + { + "code": "2914505", + "population": 28043, + "ageBands": { + "0-17": 6939, + "18-29": 4758, + "30-59": 11601, + "60+": 4745 + }, + "sexShareFemale": 0.519987, + "medianAge": 35 + }, + { + "code": "2914604", + "population": 74507, + "ageBands": { + "0-17": 19554, + "18-29": 14739, + "30-59": 31361, + "60+": 8853 + }, + "sexShareFemale": 0.520434, + "medianAge": 32 + }, + { + "code": "2914653", + "population": 28165, + "ageBands": { + "0-17": 8426, + "18-29": 5265, + "30-59": 10868, + "60+": 3606 + }, + "sexShareFemale": 0.496574, + "medianAge": 30 + }, + { + "code": "2914703", + "population": 65073, + "ageBands": { + "0-17": 16627, + "18-29": 11807, + "30-59": 26992, + "60+": 9647 + }, + "sexShareFemale": 0.517342, + "medianAge": 34 + }, + { + "code": "2914802", + "population": 186708, + "ageBands": { + "0-17": 41498, + "18-29": 31997, + "30-59": 80428, + "60+": 32785 + }, + "sexShareFemale": 0.534664, + "medianAge": 37 + }, + { + "code": "2914901", + "population": 27704, + "ageBands": { + "0-17": 8109, + "18-29": 5368, + "30-59": 11131, + "60+": 3096 + }, + "sexShareFemale": 0.491734, + "medianAge": 30 + }, + { + "code": "2915007", + "population": 13472, + "ageBands": { + "0-17": 3847, + "18-29": 2429, + "30-59": 5006, + "60+": 2190 + }, + "sexShareFemale": 0.499926, + "medianAge": 32 + }, + { + "code": "2915106", + "population": 13803, + "ageBands": { + "0-17": 3525, + "18-29": 2365, + "30-59": 5371, + "60+": 2542 + }, + "sexShareFemale": 0.496486, + "medianAge": 35 + }, + { + "code": "2915205", + "population": 15310, + "ageBands": { + "0-17": 3861, + "18-29": 2512, + "30-59": 6167, + "60+": 2770 + }, + "sexShareFemale": 0.497061, + "medianAge": 36 + }, + { + "code": "2915304", + "population": 6347, + "ageBands": { + "0-17": 1698, + "18-29": 989, + "30-59": 2518, + "60+": 1142 + }, + "sexShareFemale": 0.500867, + "medianAge": 35 + }, + { + "code": "2915353", + "population": 12311, + "ageBands": { + "0-17": 3611, + "18-29": 2180, + "30-59": 4708, + "60+": 1812 + }, + "sexShareFemale": 0.4881, + "medianAge": 32 + }, + { + "code": "2915403", + "population": 6037, + "ageBands": { + "0-17": 1783, + "18-29": 1013, + "30-59": 2254, + "60+": 987 + }, + "sexShareFemale": 0.504887, + "medianAge": 33 + }, + { + "code": "2915502", + "population": 18781, + "ageBands": { + "0-17": 4511, + "18-29": 3011, + "30-59": 7697, + "60+": 3562 + }, + "sexShareFemale": 0.51238, + "medianAge": 37 + }, + { + "code": "2915601", + "population": 59605, + "ageBands": { + "0-17": 15869, + "18-29": 10570, + "30-59": 23920, + "60+": 9246 + }, + "sexShareFemale": 0.507256, + "medianAge": 34 + }, + { + "code": "2915700", + "population": 7051, + "ageBands": { + "0-17": 1815, + "18-29": 1140, + "30-59": 2777, + "60+": 1319 + }, + "sexShareFemale": 0.498227, + "medianAge": 36 + }, + { + "code": "2915809", + "population": 24394, + "ageBands": { + "0-17": 5959, + "18-29": 4365, + "30-59": 10285, + "60+": 3785 + }, + "sexShareFemale": 0.495778, + "medianAge": 35 + }, + { + "code": "2915908", + "population": 5914, + "ageBands": { + "0-17": 1652, + "18-29": 1198, + "30-59": 2263, + "60+": 801 + }, + "sexShareFemale": 0.509469, + "medianAge": 31 + }, + { + "code": "2916005", + "population": 17813, + "ageBands": { + "0-17": 3920, + "18-29": 2723, + "30-59": 7261, + "60+": 3909 + }, + "sexShareFemale": 0.495762, + "medianAge": 39 + }, + { + "code": "2916104", + "population": 19789, + "ageBands": { + "0-17": 4613, + "18-29": 3378, + "30-59": 8352, + "60+": 3446 + }, + "sexShareFemale": 0.528122, + "medianAge": 37 + }, + { + "code": "2916203", + "population": 10341, + "ageBands": { + "0-17": 2572, + "18-29": 1712, + "30-59": 4353, + "60+": 1704 + }, + "sexShareFemale": 0.498308, + "medianAge": 35 + }, + { + "code": "2916302", + "population": 9174, + "ageBands": { + "0-17": 2566, + "18-29": 1510, + "30-59": 3560, + "60+": 1538 + }, + "sexShareFemale": 0.494441, + "medianAge": 34 + }, + { + "code": "2916401", + "population": 65897, + "ageBands": { + "0-17": 16587, + "18-29": 11562, + "30-59": 27423, + "60+": 10325 + }, + "sexShareFemale": 0.519432, + "medianAge": 35 + }, + { + "code": "2916500", + "population": 31679, + "ageBands": { + "0-17": 8697, + "18-29": 5834, + "30-59": 12525, + "60+": 4623 + }, + "sexShareFemale": 0.49386, + "medianAge": 32 + }, + { + "code": "2916609", + "population": 10279, + "ageBands": { + "0-17": 2682, + "18-29": 1808, + "30-59": 4156, + "60+": 1633 + }, + "sexShareFemale": 0.494503, + "medianAge": 34 + }, + { + "code": "2916708", + "population": 8153, + "ageBands": { + "0-17": 1819, + "18-29": 1424, + "30-59": 3521, + "60+": 1389 + }, + "sexShareFemale": 0.500061, + "medianAge": 36 + }, + { + "code": "2916807", + "population": 17052, + "ageBands": { + "0-17": 4231, + "18-29": 2791, + "30-59": 6981, + "60+": 3049 + }, + "sexShareFemale": 0.492376, + "medianAge": 36 + }, + { + "code": "2916856", + "population": 15737, + "ageBands": { + "0-17": 4338, + "18-29": 2973, + "30-59": 6349, + "60+": 2077 + }, + "sexShareFemale": 0.511978, + "medianAge": 32 + }, + { + "code": "2916906", + "population": 10999, + "ageBands": { + "0-17": 2560, + "18-29": 1850, + "30-59": 4439, + "60+": 2150 + }, + "sexShareFemale": 0.516956, + "medianAge": 37 + }, + { + "code": "2917003", + "population": 33872, + "ageBands": { + "0-17": 9083, + "18-29": 6267, + "30-59": 13088, + "60+": 5434 + }, + "sexShareFemale": 0.495867, + "medianAge": 33 + }, + { + "code": "2917102", + "population": 16617, + "ageBands": { + "0-17": 4082, + "18-29": 2689, + "30-59": 6505, + "60+": 3341 + }, + "sexShareFemale": 0.509117, + "medianAge": 37 + }, + { + "code": "2917201", + "population": 17914, + "ageBands": { + "0-17": 4163, + "18-29": 3150, + "30-59": 7420, + "60+": 3181 + }, + "sexShareFemale": 0.501563, + "medianAge": 36 + }, + { + "code": "2917300", + "population": 21902, + "ageBands": { + "0-17": 5809, + "18-29": 3684, + "30-59": 8980, + "60+": 3429 + }, + "sexShareFemale": 0.516209, + "medianAge": 34 + }, + { + "code": "2917334", + "population": 11118, + "ageBands": { + "0-17": 2888, + "18-29": 2216, + "30-59": 4487, + "60+": 1527 + }, + "sexShareFemale": 0.495413, + "medianAge": 32 + }, + { + "code": "2917359", + "population": 9275, + "ageBands": { + "0-17": 2322, + "18-29": 1487, + "30-59": 3831, + "60+": 1635 + }, + "sexShareFemale": 0.486038, + "medianAge": 35 + }, + { + "code": "2917409", + "population": 14436, + "ageBands": { + "0-17": 2801, + "18-29": 2381, + "30-59": 6056, + "60+": 3198 + }, + "sexShareFemale": 0.489748, + "medianAge": 39 + }, + { + "code": "2917508", + "population": 82590, + "ageBands": { + "0-17": 21086, + "18-29": 14803, + "30-59": 34410, + "60+": 12291 + }, + "sexShareFemale": 0.522158, + "medianAge": 34 + }, + { + "code": "2917607", + "population": 45964, + "ageBands": { + "0-17": 12205, + "18-29": 8336, + "30-59": 18135, + "60+": 7288 + }, + "sexShareFemale": 0.517253, + "medianAge": 34 + }, + { + "code": "2917706", + "population": 32703, + "ageBands": { + "0-17": 7973, + "18-29": 5743, + "30-59": 13636, + "60+": 5351 + }, + "sexShareFemale": 0.498272, + "medianAge": 35 + }, + { + "code": "2917805", + "population": 17659, + "ageBands": { + "0-17": 4431, + "18-29": 3137, + "30-59": 7506, + "60+": 2585 + }, + "sexShareFemale": 0.500595, + "medianAge": 34 + }, + { + "code": "2917904", + "population": 9285, + "ageBands": { + "0-17": 2818, + "18-29": 1798, + "30-59": 3472, + "60+": 1197 + }, + "sexShareFemale": 0.499192, + "medianAge": 30 + }, + { + "code": "2918001", + "population": 158813, + "ageBands": { + "0-17": 39567, + "18-29": 27462, + "30-59": 66669, + "60+": 25115 + }, + "sexShareFemale": 0.520789, + "medianAge": 35 + }, + { + "code": "2918100", + "population": 37626, + "ageBands": { + "0-17": 9769, + "18-29": 6876, + "30-59": 15000, + "60+": 5981 + }, + "sexShareFemale": 0.505209, + "medianAge": 34 + }, + { + "code": "2918209", + "population": 13629, + "ageBands": { + "0-17": 3286, + "18-29": 2347, + "30-59": 5820, + "60+": 2176 + }, + "sexShareFemale": 0.500624, + "medianAge": 35 + }, + { + "code": "2918308", + "population": 14355, + "ageBands": { + "0-17": 3548, + "18-29": 2474, + "30-59": 5730, + "60+": 2603 + }, + "sexShareFemale": 0.493487, + "medianAge": 35 + }, + { + "code": "2918357", + "population": 24854, + "ageBands": { + "0-17": 7320, + "18-29": 4621, + "30-59": 9648, + "60+": 3265 + }, + "sexShareFemale": 0.498954, + "medianAge": 31 + }, + { + "code": "2918407", + "population": 237821, + "ageBands": { + "0-17": 68289, + "18-29": 48381, + "30-59": 95014, + "60+": 26137 + }, + "sexShareFemale": 0.511914, + "medianAge": 30 + }, + { + "code": "2918456", + "population": 9655, + "ageBands": { + "0-17": 2533, + "18-29": 1560, + "30-59": 3885, + "60+": 1677 + }, + "sexShareFemale": 0.473123, + "medianAge": 35 + }, + { + "code": "2918506", + "population": 16354, + "ageBands": { + "0-17": 4443, + "18-29": 2793, + "30-59": 6498, + "60+": 2620 + }, + "sexShareFemale": 0.486976, + "medianAge": 34 + }, + { + "code": "2918555", + "population": 5888, + "ageBands": { + "0-17": 1439, + "18-29": 995, + "30-59": 2344, + "60+": 1110 + }, + "sexShareFemale": 0.496433, + "medianAge": 36 + }, + { + "code": "2918605", + "population": 7379, + "ageBands": { + "0-17": 1374, + "18-29": 1067, + "30-59": 3183, + "60+": 1755 + }, + "sexShareFemale": 0.510367, + "medianAge": 41 + }, + { + "code": "2918704", + "population": 4075, + "ageBands": { + "0-17": 894, + "18-29": 599, + "30-59": 1672, + "60+": 910 + }, + "sexShareFemale": 0.489325, + "medianAge": 38 + }, + { + "code": "2918753", + "population": 14105, + "ageBands": { + "0-17": 3198, + "18-29": 2816, + "30-59": 5777, + "60+": 2314 + }, + "sexShareFemale": 0.49089, + "medianAge": 34 + }, + { + "code": "2918803", + "population": 21052, + "ageBands": { + "0-17": 5178, + "18-29": 3687, + "30-59": 9121, + "60+": 3066 + }, + "sexShareFemale": 0.50228, + "medianAge": 35 + }, + { + "code": "2918902", + "population": 3845, + "ageBands": { + "0-17": 946, + "18-29": 555, + "30-59": 1574, + "60+": 770 + }, + "sexShareFemale": 0.507932, + "medianAge": 38 + }, + { + "code": "2919009", + "population": 3527, + "ageBands": { + "0-17": 904, + "18-29": 558, + "30-59": 1456, + "60+": 609 + }, + "sexShareFemale": 0.502694, + "medianAge": 36 + }, + { + "code": "2919058", + "population": 7494, + "ageBands": { + "0-17": 1772, + "18-29": 1277, + "30-59": 3113, + "60+": 1332 + }, + "sexShareFemale": 0.500133, + "medianAge": 36 + }, + { + "code": "2919108", + "population": 9015, + "ageBands": { + "0-17": 2268, + "18-29": 1616, + "30-59": 3556, + "60+": 1575 + }, + "sexShareFemale": 0.51381, + "medianAge": 34 + }, + { + "code": "2919157", + "population": 25736, + "ageBands": { + "0-17": 6857, + "18-29": 4734, + "30-59": 10256, + "60+": 3889 + }, + "sexShareFemale": 0.501515, + "medianAge": 33 + }, + { + "code": "2919207", + "population": 203331, + "ageBands": { + "0-17": 47931, + "18-29": 35337, + "30-59": 94593, + "60+": 25470 + }, + "sexShareFemale": 0.531385, + "medianAge": 35 + }, + { + "code": "2919306", + "population": 10774, + "ageBands": { + "0-17": 3007, + "18-29": 2091, + "30-59": 4440, + "60+": 1236 + }, + "sexShareFemale": 0.50724, + "medianAge": 31 + }, + { + "code": "2919405", + "population": 11834, + "ageBands": { + "0-17": 2671, + "18-29": 1776, + "30-59": 4863, + "60+": 2524 + }, + "sexShareFemale": 0.494761, + "medianAge": 38 + }, + { + "code": "2919504", + "population": 43903, + "ageBands": { + "0-17": 9955, + "18-29": 7583, + "30-59": 18979, + "60+": 7386 + }, + "sexShareFemale": 0.499009, + "medianAge": 36 + }, + { + "code": "2919553", + "population": 107909, + "ageBands": { + "0-17": 32888, + "18-29": 25039, + "30-59": 44439, + "60+": 5543 + }, + "sexShareFemale": 0.498513, + "medianAge": 28 + }, + { + "code": "2919603", + "population": 10454, + "ageBands": { + "0-17": 2821, + "18-29": 1676, + "30-59": 4056, + "60+": 1901 + }, + "sexShareFemale": 0.511575, + "medianAge": 35 + }, + { + "code": "2919702", + "population": 21599, + "ageBands": { + "0-17": 4843, + "18-29": 3752, + "30-59": 9849, + "60+": 3155 + }, + "sexShareFemale": 0.512616, + "medianAge": 35 + }, + { + "code": "2919801", + "population": 41859, + "ageBands": { + "0-17": 10015, + "18-29": 7669, + "30-59": 16536, + "60+": 7639 + }, + "sexShareFemale": 0.503787, + "medianAge": 35 + }, + { + "code": "2919900", + "population": 7256, + "ageBands": { + "0-17": 2038, + "18-29": 1286, + "30-59": 2739, + "60+": 1193 + }, + "sexShareFemale": 0.499862, + "medianAge": 33 + }, + { + "code": "2919926", + "population": 18504, + "ageBands": { + "0-17": 4347, + "18-29": 3267, + "30-59": 8282, + "60+": 2608 + }, + "sexShareFemale": 0.530588, + "medianAge": 36 + }, + { + "code": "2919959", + "population": 6973, + "ageBands": { + "0-17": 1536, + "18-29": 1092, + "30-59": 2799, + "60+": 1546 + }, + "sexShareFemale": 0.493331, + "medianAge": 39 + }, + { + "code": "2920007", + "population": 8731, + "ageBands": { + "0-17": 2221, + "18-29": 1546, + "30-59": 3531, + "60+": 1433 + }, + "sexShareFemale": 0.499256, + "medianAge": 35 + }, + { + "code": "2920106", + "population": 17674, + "ageBands": { + "0-17": 4024, + "18-29": 3066, + "30-59": 7094, + "60+": 3490 + }, + "sexShareFemale": 0.503791, + "medianAge": 37 + }, + { + "code": "2920205", + "population": 15398, + "ageBands": { + "0-17": 4502, + "18-29": 2851, + "30-59": 5973, + "60+": 2072 + }, + "sexShareFemale": 0.490843, + "medianAge": 31 + }, + { + "code": "2920304", + "population": 8670, + "ageBands": { + "0-17": 1942, + "18-29": 1489, + "30-59": 3629, + "60+": 1610 + }, + "sexShareFemale": 0.490081, + "medianAge": 37 + }, + { + "code": "2920403", + "population": 13860, + "ageBands": { + "0-17": 3453, + "18-29": 2404, + "30-59": 5365, + "60+": 2638 + }, + "sexShareFemale": 0.493939, + "medianAge": 36 + }, + { + "code": "2920452", + "population": 13919, + "ageBands": { + "0-17": 3516, + "18-29": 2801, + "30-59": 5682, + "60+": 1920 + }, + "sexShareFemale": 0.485523, + "medianAge": 32 + }, + { + "code": "2920502", + "population": 27620, + "ageBands": { + "0-17": 6700, + "18-29": 4997, + "30-59": 11364, + "60+": 4559 + }, + "sexShareFemale": 0.509124, + "medianAge": 35 + }, + { + "code": "2920601", + "population": 35859, + "ageBands": { + "0-17": 8500, + "18-29": 6268, + "30-59": 15677, + "60+": 5414 + }, + "sexShareFemale": 0.511726, + "medianAge": 36 + }, + { + "code": "2920700", + "population": 24527, + "ageBands": { + "0-17": 7062, + "18-29": 4917, + "30-59": 9996, + "60+": 2552 + }, + "sexShareFemale": 0.489787, + "medianAge": 30 + }, + { + "code": "2920809", + "population": 9267, + "ageBands": { + "0-17": 2477, + "18-29": 1640, + "30-59": 3636, + "60+": 1514 + }, + "sexShareFemale": 0.500917, + "medianAge": 33 + }, + { + "code": "2920908", + "population": 13544, + "ageBands": { + "0-17": 3547, + "18-29": 2364, + "30-59": 5230, + "60+": 2403 + }, + "sexShareFemale": 0.496899, + "medianAge": 34 + }, + { + "code": "2921005", + "population": 42566, + "ageBands": { + "0-17": 11187, + "18-29": 7588, + "30-59": 18313, + "60+": 5478 + }, + "sexShareFemale": 0.522107, + "medianAge": 34 + }, + { + "code": "2921054", + "population": 10330, + "ageBands": { + "0-17": 2678, + "18-29": 1969, + "30-59": 4058, + "60+": 1625 + }, + "sexShareFemale": 0.497967, + "medianAge": 33 + }, + { + "code": "2921104", + "population": 22194, + "ageBands": { + "0-17": 5479, + "18-29": 3626, + "30-59": 8949, + "60+": 4140 + }, + "sexShareFemale": 0.49572, + "medianAge": 36 + }, + { + "code": "2921203", + "population": 24661, + "ageBands": { + "0-17": 6236, + "18-29": 3930, + "30-59": 9963, + "60+": 4532 + }, + "sexShareFemale": 0.507765, + "medianAge": 36 + }, + { + "code": "2921302", + "population": 11071, + "ageBands": { + "0-17": 2822, + "18-29": 2024, + "30-59": 4438, + "60+": 1787 + }, + "sexShareFemale": 0.512691, + "medianAge": 34 + }, + { + "code": "2921401", + "population": 15734, + "ageBands": { + "0-17": 4141, + "18-29": 2772, + "30-59": 6251, + "60+": 2570 + }, + "sexShareFemale": 0.500636, + "medianAge": 34 + }, + { + "code": "2921450", + "population": 10187, + "ageBands": { + "0-17": 2391, + "18-29": 1783, + "30-59": 4122, + "60+": 1891 + }, + "sexShareFemale": 0.483656, + "medianAge": 36 + }, + { + "code": "2921500", + "population": 47780, + "ageBands": { + "0-17": 12185, + "18-29": 8290, + "30-59": 19131, + "60+": 8174 + }, + "sexShareFemale": 0.493428, + "medianAge": 35 + }, + { + "code": "2921609", + "population": 7996, + "ageBands": { + "0-17": 2139, + "18-29": 1298, + "30-59": 3016, + "60+": 1543 + }, + "sexShareFemale": 0.487869, + "medianAge": 35 + }, + { + "code": "2921708", + "population": 33594, + "ageBands": { + "0-17": 9427, + "18-29": 5745, + "30-59": 13183, + "60+": 5239 + }, + "sexShareFemale": 0.505299, + "medianAge": 33 + }, + { + "code": "2921807", + "population": 11143, + "ageBands": { + "0-17": 2504, + "18-29": 1500, + "30-59": 4578, + "60+": 2561 + }, + "sexShareFemale": 0.507224, + "medianAge": 40 + }, + { + "code": "2921906", + "population": 12137, + "ageBands": { + "0-17": 3241, + "18-29": 2191, + "30-59": 5019, + "60+": 1686 + }, + "sexShareFemale": 0.506056, + "medianAge": 33 + }, + { + "code": "2922003", + "population": 37977, + "ageBands": { + "0-17": 10822, + "18-29": 6829, + "30-59": 15506, + "60+": 4820 + }, + "sexShareFemale": 0.504832, + "medianAge": 32 + }, + { + "code": "2922052", + "population": 13152, + "ageBands": { + "0-17": 3805, + "18-29": 2729, + "30-59": 5033, + "60+": 1585 + }, + "sexShareFemale": 0.501597, + "medianAge": 30 + }, + { + "code": "2922102", + "population": 17305, + "ageBands": { + "0-17": 4459, + "18-29": 2901, + "30-59": 6778, + "60+": 3167 + }, + "sexShareFemale": 0.507252, + "medianAge": 36 + }, + { + "code": "2922201", + "population": 7190, + "ageBands": { + "0-17": 1522, + "18-29": 1134, + "30-59": 3306, + "60+": 1228 + }, + "sexShareFemale": 0.507789, + "medianAge": 38 + }, + { + "code": "2922250", + "population": 10443, + "ageBands": { + "0-17": 3178, + "18-29": 2057, + "30-59": 3893, + "60+": 1315 + }, + "sexShareFemale": 0.473044, + "medianAge": 29 + }, + { + "code": "2922300", + "population": 28707, + "ageBands": { + "0-17": 6765, + "18-29": 4908, + "30-59": 12441, + "60+": 4593 + }, + "sexShareFemale": 0.533285, + "medianAge": 36 + }, + { + "code": "2922409", + "population": 20037, + "ageBands": { + "0-17": 4852, + "18-29": 3478, + "30-59": 8468, + "60+": 3239 + }, + "sexShareFemale": 0.519289, + "medianAge": 35 + }, + { + "code": "2922508", + "population": 27060, + "ageBands": { + "0-17": 6498, + "18-29": 4648, + "30-59": 11894, + "60+": 4020 + }, + "sexShareFemale": 0.518514, + "medianAge": 35 + }, + { + "code": "2922607", + "population": 12063, + "ageBands": { + "0-17": 3331, + "18-29": 2057, + "30-59": 4947, + "60+": 1728 + }, + "sexShareFemale": 0.495648, + "medianAge": 34 + }, + { + "code": "2922656", + "population": 18523, + "ageBands": { + "0-17": 4867, + "18-29": 3678, + "30-59": 7491, + "60+": 2487 + }, + "sexShareFemale": 0.498029, + "medianAge": 32 + }, + { + "code": "2922706", + "population": 13715, + "ageBands": { + "0-17": 3296, + "18-29": 2185, + "30-59": 5469, + "60+": 2765 + }, + "sexShareFemale": 0.49661, + "medianAge": 38 + }, + { + "code": "2922730", + "population": 7967, + "ageBands": { + "0-17": 1716, + "18-29": 1275, + "30-59": 3406, + "60+": 1570 + }, + "sexShareFemale": 0.510983, + "medianAge": 39 + }, + { + "code": "2922755", + "population": 6501, + "ageBands": { + "0-17": 1703, + "18-29": 1000, + "30-59": 2724, + "60+": 1074 + }, + "sexShareFemale": 0.483618, + "medianAge": 36 + }, + { + "code": "2922805", + "population": 7780, + "ageBands": { + "0-17": 2183, + "18-29": 1503, + "30-59": 2955, + "60+": 1139 + }, + "sexShareFemale": 0.494216, + "medianAge": 31 + }, + { + "code": "2922854", + "population": 7538, + "ageBands": { + "0-17": 1977, + "18-29": 1419, + "30-59": 2844, + "60+": 1298 + }, + "sexShareFemale": 0.497745, + "medianAge": 33 + }, + { + "code": "2922904", + "population": 24236, + "ageBands": { + "0-17": 6307, + "18-29": 4296, + "30-59": 9425, + "60+": 4208 + }, + "sexShareFemale": 0.502311, + "medianAge": 35 + }, + { + "code": "2923001", + "population": 39509, + "ageBands": { + "0-17": 11496, + "18-29": 7119, + "30-59": 15332, + "60+": 5562 + }, + "sexShareFemale": 0.509099, + "medianAge": 32 + }, + { + "code": "2923035", + "population": 11162, + "ageBands": { + "0-17": 2765, + "18-29": 1995, + "30-59": 4562, + "60+": 1840 + }, + "sexShareFemale": 0.492564, + "medianAge": 34 + }, + { + "code": "2923050", + "population": 10660, + "ageBands": { + "0-17": 2553, + "18-29": 1975, + "30-59": 4337, + "60+": 1795 + }, + "sexShareFemale": 0.505535, + "medianAge": 35 + }, + { + "code": "2923100", + "population": 22633, + "ageBands": { + "0-17": 6014, + "18-29": 3938, + "30-59": 9076, + "60+": 3605 + }, + "sexShareFemale": 0.513145, + "medianAge": 34 + }, + { + "code": "2923209", + "population": 20715, + "ageBands": { + "0-17": 5029, + "18-29": 3622, + "30-59": 7991, + "60+": 4073 + }, + "sexShareFemale": 0.494376, + "medianAge": 36 + }, + { + "code": "2923308", + "population": 7716, + "ageBands": { + "0-17": 1842, + "18-29": 1307, + "30-59": 3185, + "60+": 1382 + }, + "sexShareFemale": 0.518015, + "medianAge": 36 + }, + { + "code": "2923357", + "population": 19243, + "ageBands": { + "0-17": 5665, + "18-29": 3511, + "30-59": 7669, + "60+": 2398 + }, + "sexShareFemale": 0.495609, + "medianAge": 31 + }, + { + "code": "2923407", + "population": 20078, + "ageBands": { + "0-17": 4730, + "18-29": 3444, + "30-59": 8236, + "60+": 3668 + }, + "sexShareFemale": 0.494422, + "medianAge": 36 + }, + { + "code": "2923506", + "population": 10339, + "ageBands": { + "0-17": 2786, + "18-29": 1920, + "30-59": 4185, + "60+": 1448 + }, + "sexShareFemale": 0.506239, + "medianAge": 32 + }, + { + "code": "2923605", + "population": 20351, + "ageBands": { + "0-17": 4294, + "18-29": 3506, + "30-59": 8616, + "60+": 3935 + }, + "sexShareFemale": 0.502825, + "medianAge": 37 + }, + { + "code": "2923704", + "population": 29252, + "ageBands": { + "0-17": 8621, + "18-29": 5050, + "30-59": 11103, + "60+": 4478 + }, + "sexShareFemale": 0.497675, + "medianAge": 32 + }, + { + "code": "2923803", + "population": 26604, + "ageBands": { + "0-17": 6229, + "18-29": 5000, + "30-59": 10888, + "60+": 4487 + }, + "sexShareFemale": 0.509585, + "medianAge": 34 + }, + { + "code": "2923902", + "population": 9370, + "ageBands": { + "0-17": 2815, + "18-29": 1584, + "30-59": 3473, + "60+": 1498 + }, + "sexShareFemale": 0.493703, + "medianAge": 32 + }, + { + "code": "2924009", + "population": 112870, + "ageBands": { + "0-17": 29258, + "18-29": 20648, + "30-59": 46732, + "60+": 16232 + }, + "sexShareFemale": 0.524311, + "medianAge": 33 + }, + { + "code": "2924058", + "population": 13243, + "ageBands": { + "0-17": 3196, + "18-29": 2154, + "30-59": 5470, + "60+": 2423 + }, + "sexShareFemale": 0.509703, + "medianAge": 36 + }, + { + "code": "2924108", + "population": 6235, + "ageBands": { + "0-17": 1540, + "18-29": 1119, + "30-59": 2427, + "60+": 1149 + }, + "sexShareFemale": 0.517723, + "medianAge": 35 + }, + { + "code": "2924207", + "population": 13954, + "ageBands": { + "0-17": 4077, + "18-29": 2843, + "30-59": 5456, + "60+": 1578 + }, + "sexShareFemale": 0.498208, + "medianAge": 30 + }, + { + "code": "2924306", + "population": 20086, + "ageBands": { + "0-17": 5112, + "18-29": 3307, + "30-59": 7758, + "60+": 3909 + }, + "sexShareFemale": 0.513641, + "medianAge": 35 + }, + { + "code": "2924405", + "population": 35357, + "ageBands": { + "0-17": 11121, + "18-29": 6421, + "30-59": 12900, + "60+": 4915 + }, + "sexShareFemale": 0.490115, + "medianAge": 30 + }, + { + "code": "2924504", + "population": 14731, + "ageBands": { + "0-17": 3423, + "18-29": 2404, + "30-59": 6039, + "60+": 2865 + }, + "sexShareFemale": 0.492838, + "medianAge": 37 + }, + { + "code": "2924603", + "population": 19083, + "ageBands": { + "0-17": 5261, + "18-29": 3398, + "30-59": 7532, + "60+": 2892 + }, + "sexShareFemale": 0.503432, + "medianAge": 33 + }, + { + "code": "2924652", + "population": 10325, + "ageBands": { + "0-17": 2336, + "18-29": 1957, + "30-59": 4076, + "60+": 1956 + }, + "sexShareFemale": 0.500726, + "medianAge": 35 + }, + { + "code": "2924678", + "population": 10974, + "ageBands": { + "0-17": 2825, + "18-29": 2283, + "30-59": 4532, + "60+": 1334 + }, + "sexShareFemale": 0.487425, + "medianAge": 32 + }, + { + "code": "2924702", + "population": 9152, + "ageBands": { + "0-17": 2103, + "18-29": 1337, + "30-59": 3677, + "60+": 2035 + }, + "sexShareFemale": 0.510708, + "medianAge": 39 + }, + { + "code": "2924801", + "population": 17566, + "ageBands": { + "0-17": 4461, + "18-29": 2900, + "30-59": 6898, + "60+": 3307 + }, + "sexShareFemale": 0.507799, + "medianAge": 36 + }, + { + "code": "2924900", + "population": 8022, + "ageBands": { + "0-17": 1897, + "18-29": 1268, + "30-59": 3310, + "60+": 1547 + }, + "sexShareFemale": 0.485914, + "medianAge": 37 + }, + { + "code": "2925006", + "population": 23334, + "ageBands": { + "0-17": 5885, + "18-29": 3993, + "30-59": 9442, + "60+": 4014 + }, + "sexShareFemale": 0.515514, + "medianAge": 35 + }, + { + "code": "2925105", + "population": 48293, + "ageBands": { + "0-17": 12384, + "18-29": 8718, + "30-59": 18642, + "60+": 8549 + }, + "sexShareFemale": 0.512435, + "medianAge": 34 + }, + { + "code": "2925204", + "population": 32136, + "ageBands": { + "0-17": 7837, + "18-29": 5429, + "30-59": 14450, + "60+": 4420 + }, + "sexShareFemale": 0.531927, + "medianAge": 35 + }, + { + "code": "2925253", + "population": 17938, + "ageBands": { + "0-17": 4911, + "18-29": 3367, + "30-59": 7008, + "60+": 2652 + }, + "sexShareFemale": 0.505853, + "medianAge": 32 + }, + { + "code": "2925303", + "population": 168326, + "ageBands": { + "0-17": 48481, + "18-29": 34456, + "30-59": 69723, + "60+": 15666 + }, + "sexShareFemale": 0.508715, + "medianAge": 30 + }, + { + "code": "2925402", + "population": 10274, + "ageBands": { + "0-17": 2762, + "18-29": 1864, + "30-59": 4083, + "60+": 1565 + }, + "sexShareFemale": 0.495523, + "medianAge": 33 + }, + { + "code": "2925501", + "population": 35003, + "ageBands": { + "0-17": 10352, + "18-29": 6554, + "30-59": 13341, + "60+": 4756 + }, + "sexShareFemale": 0.494243, + "medianAge": 31 + }, + { + "code": "2925600", + "population": 15130, + "ageBands": { + "0-17": 3544, + "18-29": 2489, + "30-59": 6537, + "60+": 2560 + }, + "sexShareFemale": 0.498612, + "medianAge": 37 + }, + { + "code": "2925709", + "population": 12621, + "ageBands": { + "0-17": 2808, + "18-29": 2077, + "30-59": 5104, + "60+": 2632 + }, + "sexShareFemale": 0.491245, + "medianAge": 38 + }, + { + "code": "2925758", + "population": 27734, + "ageBands": { + "0-17": 7897, + "18-29": 5445, + "30-59": 11065, + "60+": 3327 + }, + "sexShareFemale": 0.495637, + "medianAge": 31 + }, + { + "code": "2925808", + "population": 25988, + "ageBands": { + "0-17": 6522, + "18-29": 4758, + "30-59": 10251, + "60+": 4457 + }, + "sexShareFemale": 0.506195, + "medianAge": 35 + }, + { + "code": "2925907", + "population": 25272, + "ageBands": { + "0-17": 6288, + "18-29": 4451, + "30-59": 9967, + "60+": 4566 + }, + "sexShareFemale": 0.489316, + "medianAge": 35 + }, + { + "code": "2925931", + "population": 9461, + "ageBands": { + "0-17": 2183, + "18-29": 1616, + "30-59": 3863, + "60+": 1799 + }, + "sexShareFemale": 0.495719, + "medianAge": 36 + }, + { + "code": "2925956", + "population": 19662, + "ageBands": { + "0-17": 4992, + "18-29": 3504, + "30-59": 7482, + "60+": 3684 + }, + "sexShareFemale": 0.518309, + "medianAge": 35 + }, + { + "code": "2926004", + "population": 40586, + "ageBands": { + "0-17": 10952, + "18-29": 7109, + "30-59": 16078, + "60+": 6447 + }, + "sexShareFemale": 0.501331, + "medianAge": 34 + }, + { + "code": "2926103", + "population": 13651, + "ageBands": { + "0-17": 3480, + "18-29": 2276, + "30-59": 5693, + "60+": 2202 + }, + "sexShareFemale": 0.510219, + "medianAge": 35 + }, + { + "code": "2926202", + "population": 21642, + "ageBands": { + "0-17": 6076, + "18-29": 3692, + "30-59": 8501, + "60+": 3373 + }, + "sexShareFemale": 0.478837, + "medianAge": 33 + }, + { + "code": "2926301", + "population": 33386, + "ageBands": { + "0-17": 7655, + "18-29": 5620, + "30-59": 13929, + "60+": 6182 + }, + "sexShareFemale": 0.515366, + "medianAge": 37 + }, + { + "code": "2926400", + "population": 30711, + "ageBands": { + "0-17": 7510, + "18-29": 5484, + "30-59": 12379, + "60+": 5338 + }, + "sexShareFemale": 0.497346, + "medianAge": 35 + }, + { + "code": "2926509", + "population": 13841, + "ageBands": { + "0-17": 3587, + "18-29": 2669, + "30-59": 5413, + "60+": 2172 + }, + "sexShareFemale": 0.49245, + "medianAge": 33 + }, + { + "code": "2926608", + "population": 54010, + "ageBands": { + "0-17": 13519, + "18-29": 10133, + "30-59": 21835, + "60+": 8523 + }, + "sexShareFemale": 0.511757, + "medianAge": 34 + }, + { + "code": "2926657", + "population": 9740, + "ageBands": { + "0-17": 2538, + "18-29": 1690, + "30-59": 3933, + "60+": 1579 + }, + "sexShareFemale": 0.473717, + "medianAge": 35 + }, + { + "code": "2926707", + "population": 13184, + "ageBands": { + "0-17": 2762, + "18-29": 1993, + "30-59": 5449, + "60+": 2980 + }, + "sexShareFemale": 0.509481, + "medianAge": 39 + }, + { + "code": "2926806", + "population": 13146, + "ageBands": { + "0-17": 2799, + "18-29": 2219, + "30-59": 5536, + "60+": 2592 + }, + "sexShareFemale": 0.498479, + "medianAge": 38 + }, + { + "code": "2926905", + "population": 10497, + "ageBands": { + "0-17": 2229, + "18-29": 1763, + "30-59": 4274, + "60+": 2231 + }, + "sexShareFemale": 0.508717, + "medianAge": 38 + }, + { + "code": "2927002", + "population": 35362, + "ageBands": { + "0-17": 9518, + "18-29": 6450, + "30-59": 14363, + "60+": 5031 + }, + "sexShareFemale": 0.504072, + "medianAge": 33 + }, + { + "code": "2927101", + "population": 10308, + "ageBands": { + "0-17": 3349, + "18-29": 2105, + "30-59": 3795, + "60+": 1059 + }, + "sexShareFemale": 0.507179, + "medianAge": 28 + }, + { + "code": "2927200", + "population": 28282, + "ageBands": { + "0-17": 7219, + "18-29": 5250, + "30-59": 10954, + "60+": 4859 + }, + "sexShareFemale": 0.521993, + "medianAge": 34 + }, + { + "code": "2927309", + "population": 14987, + "ageBands": { + "0-17": 3898, + "18-29": 2494, + "30-59": 6295, + "60+": 2300 + }, + "sexShareFemale": 0.521652, + "medianAge": 35 + }, + { + "code": "2927408", + "population": 2417678, + "ageBands": { + "0-17": 493892, + "18-29": 413081, + "30-59": 1111630, + "60+": 399075 + }, + "sexShareFemale": 0.544034, + "medianAge": 38 + }, + { + "code": "2927507", + "population": 20952, + "ageBands": { + "0-17": 5218, + "18-29": 4008, + "30-59": 8476, + "60+": 3250 + }, + "sexShareFemale": 0.507875, + "medianAge": 34 + }, + { + "code": "2927606", + "population": 14965, + "ageBands": { + "0-17": 4060, + "18-29": 2670, + "30-59": 5997, + "60+": 2238 + }, + "sexShareFemale": 0.496893, + "medianAge": 33 + }, + { + "code": "2927705", + "population": 29185, + "ageBands": { + "0-17": 8322, + "18-29": 5141, + "30-59": 12032, + "60+": 3690 + }, + "sexShareFemale": 0.504163, + "medianAge": 32 + }, + { + "code": "2927804", + "population": 4681, + "ageBands": { + "0-17": 1200, + "18-29": 747, + "30-59": 1831, + "60+": 903 + }, + "sexShareFemale": 0.500748, + "medianAge": 36 + }, + { + "code": "2927903", + "population": 10300, + "ageBands": { + "0-17": 2475, + "18-29": 1760, + "30-59": 4157, + "60+": 1908 + }, + "sexShareFemale": 0.505243, + "medianAge": 36 + }, + { + "code": "2928000", + "population": 37834, + "ageBands": { + "0-17": 9878, + "18-29": 7021, + "30-59": 15192, + "60+": 5743 + }, + "sexShareFemale": 0.506238, + "medianAge": 33 + }, + { + "code": "2928059", + "population": 13896, + "ageBands": { + "0-17": 3756, + "18-29": 2419, + "30-59": 5710, + "60+": 2011 + }, + "sexShareFemale": 0.474669, + "medianAge": 33 + }, + { + "code": "2928109", + "population": 38604, + "ageBands": { + "0-17": 9952, + "18-29": 6711, + "30-59": 15757, + "60+": 6184 + }, + "sexShareFemale": 0.500699, + "medianAge": 34 + }, + { + "code": "2928208", + "population": 24755, + "ageBands": { + "0-17": 6150, + "18-29": 4178, + "30-59": 9756, + "60+": 4671 + }, + "sexShareFemale": 0.491699, + "medianAge": 36 + }, + { + "code": "2928307", + "population": 8716, + "ageBands": { + "0-17": 2172, + "18-29": 1504, + "30-59": 3463, + "60+": 1577 + }, + "sexShareFemale": 0.525585, + "medianAge": 35 + }, + { + "code": "2928406", + "population": 27390, + "ageBands": { + "0-17": 7900, + "18-29": 4755, + "30-59": 10291, + "60+": 4444 + }, + "sexShareFemale": 0.488682, + "medianAge": 32 + }, + { + "code": "2928505", + "population": 10441, + "ageBands": { + "0-17": 2460, + "18-29": 1813, + "30-59": 4253, + "60+": 1915 + }, + "sexShareFemale": 0.516521, + "medianAge": 36 + }, + { + "code": "2928604", + "population": 56012, + "ageBands": { + "0-17": 12937, + "18-29": 9190, + "30-59": 24240, + "60+": 9645 + }, + "sexShareFemale": 0.529101, + "medianAge": 37 + }, + { + "code": "2928703", + "population": 103055, + "ageBands": { + "0-17": 24024, + "18-29": 19620, + "30-59": 46527, + "60+": 12884 + }, + "sexShareFemale": 0.533404, + "medianAge": 34 + }, + { + "code": "2928802", + "population": 52276, + "ageBands": { + "0-17": 14362, + "18-29": 9903, + "30-59": 20635, + "60+": 7376 + }, + "sexShareFemale": 0.52571, + "medianAge": 32 + }, + { + "code": "2928901", + "population": 32828, + "ageBands": { + "0-17": 9933, + "18-29": 6779, + "30-59": 12659, + "60+": 3457 + }, + "sexShareFemale": 0.485957, + "medianAge": 29 + }, + { + "code": "2928950", + "population": 8426, + "ageBands": { + "0-17": 1860, + "18-29": 1308, + "30-59": 3544, + "60+": 1714 + }, + "sexShareFemale": 0.508782, + "medianAge": 39 + }, + { + "code": "2929008", + "population": 11026, + "ageBands": { + "0-17": 2535, + "18-29": 1896, + "30-59": 4763, + "60+": 1832 + }, + "sexShareFemale": 0.522492, + "medianAge": 37 + }, + { + "code": "2929057", + "population": 15194, + "ageBands": { + "0-17": 4161, + "18-29": 2746, + "30-59": 6156, + "60+": 2131 + }, + "sexShareFemale": 0.500856, + "medianAge": 33 + }, + { + "code": "2929107", + "population": 20283, + "ageBands": { + "0-17": 4511, + "18-29": 3369, + "30-59": 8968, + "60+": 3435 + }, + "sexShareFemale": 0.511956, + "medianAge": 37 + }, + { + "code": "2929206", + "population": 38733, + "ageBands": { + "0-17": 10176, + "18-29": 7243, + "30-59": 16858, + "60+": 4456 + }, + "sexShareFemale": 0.529987, + "medianAge": 33 + }, + { + "code": "2929255", + "population": 18600, + "ageBands": { + "0-17": 4879, + "18-29": 3238, + "30-59": 7572, + "60+": 2911 + }, + "sexShareFemale": 0.492097, + "medianAge": 34 + }, + { + "code": "2929305", + "population": 39513, + "ageBands": { + "0-17": 10143, + "18-29": 7110, + "30-59": 17096, + "60+": 5164 + }, + "sexShareFemale": 0.527953, + "medianAge": 34 + }, + { + "code": "2929354", + "population": 5315, + "ageBands": { + "0-17": 1392, + "18-29": 856, + "30-59": 2106, + "60+": 961 + }, + "sexShareFemale": 0.496143, + "medianAge": 35 + }, + { + "code": "2929370", + "population": 10187, + "ageBands": { + "0-17": 2662, + "18-29": 1744, + "30-59": 4021, + "60+": 1760 + }, + "sexShareFemale": 0.505252, + "medianAge": 35 + }, + { + "code": "2929404", + "population": 10334, + "ageBands": { + "0-17": 2343, + "18-29": 1877, + "30-59": 4357, + "60+": 1757 + }, + "sexShareFemale": 0.500194, + "medianAge": 36 + }, + { + "code": "2929503", + "population": 40958, + "ageBands": { + "0-17": 9638, + "18-29": 6922, + "30-59": 18061, + "60+": 6337 + }, + "sexShareFemale": 0.526515, + "medianAge": 36 + }, + { + "code": "2929602", + "population": 17963, + "ageBands": { + "0-17": 4220, + "18-29": 3154, + "30-59": 7793, + "60+": 2796 + }, + "sexShareFemale": 0.517787, + "medianAge": 35 + }, + { + "code": "2929701", + "population": 16008, + "ageBands": { + "0-17": 4337, + "18-29": 3039, + "30-59": 6143, + "60+": 2489 + }, + "sexShareFemale": 0.505185, + "medianAge": 33 + }, + { + "code": "2929750", + "population": 11438, + "ageBands": { + "0-17": 2680, + "18-29": 1702, + "30-59": 4912, + "60+": 2144 + }, + "sexShareFemale": 0.525616, + "medianAge": 38 + }, + { + "code": "2929800", + "population": 10478, + "ageBands": { + "0-17": 2676, + "18-29": 1648, + "30-59": 4134, + "60+": 2020 + }, + "sexShareFemale": 0.511739, + "medianAge": 37 + }, + { + "code": "2929909", + "population": 46160, + "ageBands": { + "0-17": 12172, + "18-29": 9073, + "30-59": 18310, + "60+": 6605 + }, + "sexShareFemale": 0.51263, + "medianAge": 32 + }, + { + "code": "2930006", + "population": 9360, + "ageBands": { + "0-17": 2288, + "18-29": 1482, + "30-59": 3887, + "60+": 1703 + }, + "sexShareFemale": 0.494872, + "medianAge": 36 + }, + { + "code": "2930105", + "population": 74523, + "ageBands": { + "0-17": 18886, + "18-29": 13064, + "30-59": 30939, + "60+": 11634 + }, + "sexShareFemale": 0.522, + "medianAge": 35 + }, + { + "code": "2930154", + "population": 34222, + "ageBands": { + "0-17": 10616, + "18-29": 6288, + "30-59": 12769, + "60+": 4549 + }, + "sexShareFemale": 0.492344, + "medianAge": 30 + }, + { + "code": "2930204", + "population": 38154, + "ageBands": { + "0-17": 11650, + "18-29": 7029, + "30-59": 14519, + "60+": 4956 + }, + "sexShareFemale": 0.484222, + "medianAge": 30 + }, + { + "code": "2930303", + "population": 17066, + "ageBands": { + "0-17": 4213, + "18-29": 2601, + "30-59": 7036, + "60+": 3216 + }, + "sexShareFemale": 0.487402, + "medianAge": 37 + }, + { + "code": "2930402", + "population": 17996, + "ageBands": { + "0-17": 4098, + "18-29": 3260, + "30-59": 7259, + "60+": 3379 + }, + "sexShareFemale": 0.516948, + "medianAge": 36 + }, + { + "code": "2930501", + "population": 80435, + "ageBands": { + "0-17": 20421, + "18-29": 14858, + "30-59": 33908, + "60+": 11248 + }, + "sexShareFemale": 0.521713, + "medianAge": 34 + }, + { + "code": "2930600", + "population": 13335, + "ageBands": { + "0-17": 3263, + "18-29": 2123, + "30-59": 5462, + "60+": 2487 + }, + "sexShareFemale": 0.511961, + "medianAge": 36 + }, + { + "code": "2930709", + "population": 114559, + "ageBands": { + "0-17": 30150, + "18-29": 21250, + "30-59": 49829, + "60+": 13330 + }, + "sexShareFemale": 0.525066, + "medianAge": 33 + }, + { + "code": "2930758", + "population": 13408, + "ageBands": { + "0-17": 4262, + "18-29": 2584, + "30-59": 4919, + "60+": 1643 + }, + "sexShareFemale": 0.486202, + "medianAge": 29 + }, + { + "code": "2930766", + "population": 14773, + "ageBands": { + "0-17": 3313, + "18-29": 2879, + "30-59": 6003, + "60+": 2578 + }, + "sexShareFemale": 0.492656, + "medianAge": 35 + }, + { + "code": "2930774", + "population": 25475, + "ageBands": { + "0-17": 7335, + "18-29": 4907, + "30-59": 9788, + "60+": 3445 + }, + "sexShareFemale": 0.510304, + "medianAge": 31 + }, + { + "code": "2930808", + "population": 17054, + "ageBands": { + "0-17": 4591, + "18-29": 3574, + "30-59": 6586, + "60+": 2303 + }, + "sexShareFemale": 0.500938, + "medianAge": 31 + }, + { + "code": "2930907", + "population": 11979, + "ageBands": { + "0-17": 3077, + "18-29": 2168, + "30-59": 4717, + "60+": 2017 + }, + "sexShareFemale": 0.484515, + "medianAge": 34 + }, + { + "code": "2931004", + "population": 21006, + "ageBands": { + "0-17": 4843, + "18-29": 3502, + "30-59": 8884, + "60+": 3777 + }, + "sexShareFemale": 0.498429, + "medianAge": 36 + }, + { + "code": "2931053", + "population": 17158, + "ageBands": { + "0-17": 4318, + "18-29": 3272, + "30-59": 6763, + "60+": 2805 + }, + "sexShareFemale": 0.501748, + "medianAge": 34 + }, + { + "code": "2931103", + "population": 7717, + "ageBands": { + "0-17": 1790, + "18-29": 1441, + "30-59": 3047, + "60+": 1439 + }, + "sexShareFemale": 0.514189, + "medianAge": 36 + }, + { + "code": "2931202", + "population": 18044, + "ageBands": { + "0-17": 5161, + "18-29": 3267, + "30-59": 7136, + "60+": 2480 + }, + "sexShareFemale": 0.501607, + "medianAge": 32 + }, + { + "code": "2931301", + "population": 15818, + "ageBands": { + "0-17": 4528, + "18-29": 3137, + "30-59": 6110, + "60+": 2043 + }, + "sexShareFemale": 0.504046, + "medianAge": 30 + }, + { + "code": "2931350", + "population": 145216, + "ageBands": { + "0-17": 38258, + "18-29": 27127, + "30-59": 60099, + "60+": 19732 + }, + "sexShareFemale": 0.517932, + "medianAge": 33 + }, + { + "code": "2931400", + "population": 7110, + "ageBands": { + "0-17": 1549, + "18-29": 1254, + "30-59": 3075, + "60+": 1232 + }, + "sexShareFemale": 0.517018, + "medianAge": 37 + }, + { + "code": "2931509", + "population": 21176, + "ageBands": { + "0-17": 5630, + "18-29": 3878, + "30-59": 8528, + "60+": 3140 + }, + "sexShareFemale": 0.507934, + "medianAge": 33 + }, + { + "code": "2931608", + "population": 15332, + "ageBands": { + "0-17": 4259, + "18-29": 3162, + "30-59": 5984, + "60+": 1927 + }, + "sexShareFemale": 0.485977, + "medianAge": 30 + }, + { + "code": "2931707", + "population": 10798, + "ageBands": { + "0-17": 2564, + "18-29": 1991, + "30-59": 4453, + "60+": 1790 + }, + "sexShareFemale": 0.525097, + "medianAge": 36 + }, + { + "code": "2931806", + "population": 16296, + "ageBands": { + "0-17": 3577, + "18-29": 2457, + "30-59": 6580, + "60+": 3682 + }, + "sexShareFemale": 0.496011, + "medianAge": 39 + }, + { + "code": "2931905", + "population": 48736, + "ageBands": { + "0-17": 11712, + "18-29": 8456, + "30-59": 20426, + "60+": 8142 + }, + "sexShareFemale": 0.504473, + "medianAge": 35 + }, + { + "code": "2932002", + "population": 24665, + "ageBands": { + "0-17": 6078, + "18-29": 3846, + "30-59": 10122, + "60+": 4619 + }, + "sexShareFemale": 0.495358, + "medianAge": 37 + }, + { + "code": "2932101", + "population": 18626, + "ageBands": { + "0-17": 4584, + "18-29": 2884, + "30-59": 7755, + "60+": 3403 + }, + "sexShareFemale": 0.500376, + "medianAge": 36 + }, + { + "code": "2932200", + "population": 17596, + "ageBands": { + "0-17": 4817, + "18-29": 2960, + "30-59": 6939, + "60+": 2880 + }, + "sexShareFemale": 0.519152, + "medianAge": 34 + }, + { + "code": "2932309", + "population": 16094, + "ageBands": { + "0-17": 4226, + "18-29": 2569, + "30-59": 6244, + "60+": 3055 + }, + "sexShareFemale": 0.516341, + "medianAge": 36 + }, + { + "code": "2932408", + "population": 13432, + "ageBands": { + "0-17": 3027, + "18-29": 2220, + "30-59": 5784, + "60+": 2401 + }, + "sexShareFemale": 0.494491, + "medianAge": 37 + }, + { + "code": "2932457", + "population": 13642, + "ageBands": { + "0-17": 3899, + "18-29": 2529, + "30-59": 5398, + "60+": 1816 + }, + "sexShareFemale": 0.489811, + "medianAge": 31 + }, + { + "code": "2932507", + "population": 18131, + "ageBands": { + "0-17": 4663, + "18-29": 2805, + "30-59": 7350, + "60+": 3313 + }, + "sexShareFemale": 0.498924, + "medianAge": 36 + }, + { + "code": "2932606", + "population": 15355, + "ageBands": { + "0-17": 3538, + "18-29": 2654, + "30-59": 6316, + "60+": 2847 + }, + "sexShareFemale": 0.499446, + "medianAge": 36 + }, + { + "code": "2932705", + "population": 21420, + "ageBands": { + "0-17": 5858, + "18-29": 3578, + "30-59": 8632, + "60+": 3352 + }, + "sexShareFemale": 0.506676, + "medianAge": 34 + }, + { + "code": "2932804", + "population": 16277, + "ageBands": { + "0-17": 4420, + "18-29": 3058, + "30-59": 6391, + "60+": 2408 + }, + "sexShareFemale": 0.504331, + "medianAge": 32 + }, + { + "code": "2932903", + "population": 85655, + "ageBands": { + "0-17": 21598, + "18-29": 15646, + "30-59": 36102, + "60+": 12309 + }, + "sexShareFemale": 0.5183, + "medianAge": 34 + }, + { + "code": "2933000", + "population": 24362, + "ageBands": { + "0-17": 5751, + "18-29": 4019, + "30-59": 10544, + "60+": 4048 + }, + "sexShareFemale": 0.514572, + "medianAge": 36 + }, + { + "code": "2933059", + "population": 13800, + "ageBands": { + "0-17": 3216, + "18-29": 2383, + "30-59": 5587, + "60+": 2614 + }, + "sexShareFemale": 0.506087, + "medianAge": 36 + }, + { + "code": "2933109", + "population": 8101, + "ageBands": { + "0-17": 1821, + "18-29": 1277, + "30-59": 3360, + "60+": 1643 + }, + "sexShareFemale": 0.508209, + "medianAge": 38 + }, + { + "code": "2933158", + "population": 13377, + "ageBands": { + "0-17": 3639, + "18-29": 2296, + "30-59": 5255, + "60+": 2187 + }, + "sexShareFemale": 0.501682, + "medianAge": 33 + }, + { + "code": "2933174", + "population": 9913, + "ageBands": { + "0-17": 2288, + "18-29": 1704, + "30-59": 4281, + "60+": 1640 + }, + "sexShareFemale": 0.502169, + "medianAge": 36 + }, + { + "code": "2933208", + "population": 42529, + "ageBands": { + "0-17": 9661, + "18-29": 6943, + "30-59": 18070, + "60+": 7855 + }, + "sexShareFemale": 0.516259, + "medianAge": 38 + }, + { + "code": "2933257", + "population": 6003, + "ageBands": { + "0-17": 1473, + "18-29": 948, + "30-59": 2415, + "60+": 1167 + }, + "sexShareFemale": 0.48026, + "medianAge": 37 + }, + { + "code": "2933307", + "population": 370879, + "ageBands": { + "0-17": 91170, + "18-29": 72665, + "30-59": 155901, + "60+": 51143 + }, + "sexShareFemale": 0.521647, + "medianAge": 33 + }, + { + "code": "2933406", + "population": 9503, + "ageBands": { + "0-17": 2349, + "18-29": 1844, + "30-59": 3800, + "60+": 1510 + }, + "sexShareFemale": 0.50784, + "medianAge": 33 + }, + { + "code": "2933455", + "population": 12968, + "ageBands": { + "0-17": 3510, + "18-29": 2406, + "30-59": 5052, + "60+": 2000 + }, + "sexShareFemale": 0.481801, + "medianAge": 33 + }, + { + "code": "2933505", + "population": 24474, + "ageBands": { + "0-17": 7100, + "18-29": 4356, + "30-59": 9740, + "60+": 3278 + }, + "sexShareFemale": 0.492972, + "medianAge": 32 + }, + { + "code": "2933604", + "population": 44757, + "ageBands": { + "0-17": 13790, + "18-29": 8312, + "30-59": 16418, + "60+": 6237 + }, + "sexShareFemale": 0.511026, + "medianAge": 30 + } + ], + "evidenceSha256": "21da75c798416f474db841fa04ff08f63f70a2ebdf3dd54cc0bb26eef98be99c" +} diff --git a/tests/int/bahiaMunicipalityDemographics.int.spec.ts b/tests/int/bahiaMunicipalityDemographics.int.spec.ts new file mode 100644 index 00000000..39ce7bfb --- /dev/null +++ b/tests/int/bahiaMunicipalityDemographics.int.spec.ts @@ -0,0 +1,102 @@ +// @vitest-environment node + +import { createHash } from 'node:crypto' +import { readFileSync } from 'node:fs' + +import { describe, expect, it } from 'vitest' + +import { + bahiaMunicipalityDemographics, + demographicsForCode, + demographicsForMunicipalityName, +} from '@/lib/bahiaMunicipalityDemographics' +import { bahiaMunicipalityCodes } from '@/lib/bahiaMunicipalityCodes' +import { bahiaMunicipalities } from '@/lib/bahiaTerritories' + +type OfficialEvidence = { + municipalityCount: number + assignments: Array<{ + code: string + population: number + ageBands: Record<'0-17' | '18-29' | '30-59' | '60+', number> + sexShareFemale: number + medianAge: number | null + }> + evidenceSha256: string +} + +const officialEvidence = JSON.parse( + readFileSync( + new URL('../fixtures/bahia-municipality-demographics.official.json', import.meta.url), + 'utf8', + ), +) as OfficialEvidence + +describe('Bahia municipality demographics (IBGE Censo 2022)', () => { + it('covers all 417 Bahia municipalities with positive population', () => { + expect(Object.keys(bahiaMunicipalityDemographics)).toHaveLength(417) + expect(bahiaMunicipalities).toHaveLength(417) + expect(new Set(Object.keys(bahiaMunicipalityDemographics))).toHaveLength(417) + + for (const city of bahiaMunicipalities) { + const code = bahiaMunicipalityCodes[city] + const record = bahiaMunicipalityDemographics[code] + expect(record, city).toBeDefined() + expect(record.population).toBeGreaterThan(0) + expect( + record.ageBands['0-17'] + + record.ageBands['18-29'] + + record.ageBands['30-59'] + + record.ageBands['60+'], + ).toBe(record.population) + expect(record.sexShareFemale).toBeGreaterThan(0) + expect(record.sexShareFemale).toBeLessThanOrEqual(1) + } + }) + + it('matches the independently downloaded official evidence fixture', () => { + expect(officialEvidence.municipalityCount).toBe(417) + expect( + officialEvidence.assignments.map( + ({ code, population, ageBands, sexShareFemale, medianAge }) => ({ + code, + population, + ageBands, + sexShareFemale, + medianAge, + }), + ), + ).toEqual( + Object.entries(bahiaMunicipalityDemographics) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([code, record]) => ({ + code, + population: record.population, + ageBands: record.ageBands, + sexShareFemale: record.sexShareFemale, + medianAge: record.medianAge, + })), + ) + }) + + it('matches every official assignment by a fixed evidence checksum', () => { + const assignmentRows = Object.entries(bahiaMunicipalityDemographics) + .sort(([left], [right]) => left.localeCompare(right)) + .map( + ([code, record]) => + `D\t${code}\t${record.population}\t${record.ageBands['0-17']}\t${record.ageBands['18-29']}\t${record.ageBands['30-59']}\t${record.ageBands['60+']}\t${record.sexShareFemale}\t${record.medianAge ?? ''}\n`, + ) + .join('') + + expect(createHash('sha256').update(assignmentRows).digest('hex')).toBe( + officialEvidence.evidenceSha256, + ) + }) + + it('supports bidirectional lookups by municipality name', () => { + const salvador = demographicsForMunicipalityName('Salvador') + expect(salvador).toBeDefined() + expect(demographicsForCode('2927408')).toEqual(salvador) + expect(demographicsForMunicipalityName('Município inexistente')).toBeUndefined() + }) +}) diff --git a/tests/unit/nucleusIbgeVoterProfile.unit.spec.ts b/tests/unit/nucleusIbgeVoterProfile.unit.spec.ts new file mode 100644 index 00000000..80cc4cee --- /dev/null +++ b/tests/unit/nucleusIbgeVoterProfile.unit.spec.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest' + +import { + getNucleusIbgeVoterProfile, + IBGE_VOTER_PROFILE_LABEL, +} from '@/utilities/nucleusIbgeVoterProfile' + +describe('getNucleusIbgeVoterProfile', () => { + it('returns a profile for a single municipality', () => { + const result = getNucleusIbgeVoterProfile({ cities: ['Salvador'], regions: [] }) + expect(result.status).toBe('available') + if (result.status !== 'available') return + + expect(result.profile.label).toBe(IBGE_VOTER_PROFILE_LABEL) + expect(result.profile.notes).toContain('habitantes') + expect(result.profile.ageRange).toContain('0 a 17 anos') + expect(result.profile.localTraits).toContain('Predominam moradores') + expect(result.profile.notes).toContain('município') + }) + + it('weights multiple municipalities by population', () => { + const combined = getNucleusIbgeVoterProfile({ + cities: ['Salvador', 'Abaíra'], + regions: [], + }) + + expect(combined.status).toBe('available') + if (combined.status !== 'available') return + + expect(combined.profile.notes).toContain('2 municípios') + }) + + it('resolves cities from identity territories when cities are empty', () => { + const result = getNucleusIbgeVoterProfile({ + cities: [], + regions: ['Metropolitano de Salvador'], + }) + + expect(result.status).toBe('available') + }) + + it('returns semPerfil when geography is missing', () => { + expect(getNucleusIbgeVoterProfile({ cities: [], regions: [] })).toEqual({ + status: 'semPerfil', + }) + }) + + it('returns semPerfil for unknown municipalities', () => { + expect( + getNucleusIbgeVoterProfile({ cities: ['Município inexistente'], regions: [] }), + ).toEqual({ status: 'semPerfil' }) + }) +})