D-2 — Georreferenciamento
Parte do Ciclo de Demandas — Fase 1
Propósito
Seção intitulada “Propósito”A D-2 resolve a questão territorial das demandas normalizadas: a qual unidade cívica essa demanda pertence? E a qual nível? Consome demanda.normalizada da D-1b e produz demanda.georreferenciada. O evento de saída é consumido pela D-3 (categorização), D-4 (priorização e ranking) e D-7 (transparência). Sem território resolvido, o restante do pipeline (ranquear, agendar, atribuir conselheiro) não funciona.
Não categoriza o conteúdo da demanda. Não prioriza. Não valida vínculo. Apenas resolve a geometria territorial: dado um ponto (coordenadas GPS), determina qual unidade cívica o contém e a cadeia completa de UCs pai.
É análoga à L-2 do pipeline de lugares, porém mais simples. Não faz enriquecimento de tipificação, não consulta bases externas e não lida com subtipos de lugar. Apenas point-in-polygon e cache.
A D-2 é uma colônia pura de eventos. Não tem BFF acoplado, não expõe endpoints REST e não faz chamadas síncronas a outras colônias.
1. Estrutura do Módulo NestJS
Seção intitulada “1. Estrutura do Módulo NestJS”A D-2 é um módulo NestJS com encapsulamento próprio dentro do monolito modular do MVP. Consome eventos do barramento via EventBusService (N-0a) e publica eventos ao final do processamento. Não expõe controllers REST.
1.1 Árvore de diretórios
Seção intitulada “1.1 Árvore de diretórios”src/demanda/d-2-georreferenciamento/├── d2.module.ts # Module definition├── d2.service.ts # Lógica de negócio: resolver UC, persistir, publicar├── d2.constants.ts # Limites de coordenadas, TTLs e precisão do cache├── repositories/│ ├── resolucao-geo.repository.ts # Acesso a d2.resolucoes_geo e d2.revisoes_geo_pendentes│ ├── evento-processado.repository.ts # Acesso a d2.eventos_processados (idempotência)│ ├── consumer-offset.repository.ts # Acesso a d2.consumer_offset (cursor de replay)│ └── core-geo.repository.ts # Leitura de core.uc_polygons com snapshot em memória└── services/ ├── point-in-polygon.service.ts # Point-in-polygon (Turf.js) com pré-filtro de bbox └── geo-cache.service.ts # Cache de resoluções geo (in-memory)Os testes ficam ao lado dos arquivos testados: d2.service.spec.ts, geo-cache.service.spec.ts, point-in-polygon.service.spec.ts e core-geo.repository.spec.ts.
1.2 Module definition
Seção intitulada “1.2 Module definition”@Module({ imports: [], controllers: [], // Colônia pura de eventos — sem REST providers: [ D2Service, ResolucaoGeoRepository, EventoProcessadoRepository, ConsumerOffsetRepository, CoreGeoRepository, PointInPolygonService, GeoCacheService, ], exports: [],})export class D2Module implements OnModuleInit { constructor(private readonly d2Service: D2Service) {}
async onModuleInit(): Promise<void> { await this.d2Service.iniciar(); }}1.3 Pontos de atenção
Seção intitulada “1.3 Pontos de atenção”- O módulo não é
@Global(). A D-2 não é dependência de nenhuma outra colônia. Outras colônias consomem seu evento (demanda.georreferenciada), não seu código. - O módulo não importa
EventBusModuleexplicitamente.EventBusModuleé@Global(), e oEventBusServiceé injetável sem import. - O módulo não importa
RegistryModule. A validação de schema dos eventos publicados é feita pelo próprio Event Bus (N-0a) no momento dopublicar(). A D-2 apenas constrói o payload conforme o contrato do Registry. - O
OnModuleInitdispara o protocolo de inicialização: seed do cursor, replay de eventos perdidos e registro de handlers. - O módulo não registra
ThrottlerModule— rate limiting é responsabilidade exclusiva do BFF (D-1a), na borda HTTP. A D-2 processa o que recebe do barramento. - O
CoreGeoRepositoryacessacore.uc_polygonsem modo somente leitura e mantém um snapshot em memória com TTL de 6 horas. É uma simplificação de MVP (ver seção 8.2). Na Fase 2, a base de polígonos migra para uma colônia dedicada que publica eventos de sincronização.
1.4 Serviço — responsabilidades e contrato
Seção intitulada “1.4 Serviço — responsabilidades e contrato”// D2Service consome 'demanda.normalizada', publica 'demanda.georreferenciada'iniciar(): Promise<void>;processarDemandaNormalizada(evento: { event_id: string; sequence_number: bigint; correlacao_id: string | null; payload: Record<string, unknown>;}): Promise<void>;registrarConsumidores(): void;O service não expõe interface formal. Nenhuma outra colônia injeta D2Service. A comunicação com o exterior é via barramento.
1.5 Colônia pura de eventos
Seção intitulada “1.5 Colônia pura de eventos”A D-2 não tem BFF acoplado. É uma colônia de processamento puro: escuta, processa, publica. O input é sempre via barramento — exclusivamente demanda.normalizada da D-1b.
1.6 Dependência de leitura do núcleo (simplificação MVP)
Seção intitulada “1.6 Dependência de leitura do núcleo (simplificação MVP)”A D-2 acessa core.uc_polygons em modo leitura para realizar point-in-polygon. A mesma decisão se aplica à L-2 do pipeline de lugares — ambas compartilham a mesma base de polígonos via leitura do schema core. Esta é uma exceção controlada à regra de isolamento, válida apenas no MVP monolito. Justificativa: a base de polígonos de UCs é um dado de infraestrutura fundamental, tão essencial quanto o Registry. Duplicá-la em cada colônia que faz point-in-polygon (D-2 e L-2) no MVP geraria complexidade de sincronização desproporcional ao benefício.
Na Fase 2, a base de polígonos migra para uma colônia dedicada de infraestrutura geoespacial que publica eventos de atualização de polígonos. D-2 e L-2 mantêm réplicas locais sincronizadas por evento. A consulta direta ao core é removida.
2. Banco de Dados — Schema e Entidades
Seção intitulada “2. Banco de Dados — Schema e Entidades”2.1 Schema d2
Seção intitulada “2.1 Schema d2”Todas as tabelas da D-2 residem no schema d2 do PostgreSQL. Este schema é de uso exclusivo do módulo D-2. Nenhuma outra colônia lê ou escreve nestas tabelas.
2.2 Tabela d2.resolucoes_geo
Seção intitulada “2.2 Tabela d2.resolucoes_geo”Registro do resultado do georreferenciamento territorial para cada demanda processada. Uma linha por demanda_id.
CREATE SCHEMA IF NOT EXISTS d2;
CREATE TABLE d2.resolucoes_geo ( id UUID NOT NULL, demanda_id UUID NOT NULL, unidade_civica_id UUID NOT NULL, nivel_minimo_resolvido INTEGER NOT NULL, cadeia_ucs UUID[], metodo_resolucao VARCHAR(20) NOT NULL, confianca_geo VARCHAR(10) NOT NULL, coordenadas_lat DOUBLE PRECISION NOT NULL, coordenadas_lng DOUBLE PRECISION NOT NULL, event_id UUID NOT NULL, correlacao_id UUID NOT NULL, criado_em TIMESTAMPTZ(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT resolucoes_geo_pkey PRIMARY KEY (id));
CREATE UNIQUE INDEX resolucoes_geo_demanda_id_key ON d2.resolucoes_geo (demanda_id);CREATE INDEX resolucoes_geo_unidade_civica_id_idx ON d2.resolucoes_geo (unidade_civica_id);CREATE INDEX resolucoes_geo_event_id_idx ON d2.resolucoes_geo (event_id);CREATE INDEX resolucoes_geo_confianca_geo_idx ON d2.resolucoes_geo (confianca_geo);O banco não tem CHECKs para confianca_geo, metodo_resolucao ou nivel_minimo_resolvido. Os valores são garantidos pelo código que preenche a linha e pelo schema do Registry na publicação.
Colunas — detalhamento
Seção intitulada “Colunas — detalhamento”| Coluna | Tipo | Descrição |
|---|---|---|
id |
UUID PK | Identificador interno da resolução. Gerado pelo Prisma. |
demanda_id |
UUID | FK lógica para o registro de demanda na D-1a/D-1b. Sem constraint formal — regra de isolamento. |
unidade_civica_id |
UUID | UC de menor nível onde o ponto está contido. FK lógica para a base de UCs. |
nivel_minimo_resolvido |
INTEGER 1-7 | Nível da UC resolvida. Se não há polígono de bairro (nível 2), retorna nível 4 (município). |
cadeia_ucs |
UUID[] | Array ordenado de UCs pai, da menor resolvida até a nacional. |
metodo_resolucao |
VARCHAR(20) | gps para coordenadas diretas do dispositivo, endereco para geocodificação de texto, inferencia para estimativa. No MVP, sempre gps. |
confianca_geo |
VARCHAR(10) | alta para GPS, media para endereço textual, baixa para inferência ou falha de geocodificação. |
coordenadas_lat / coordenadas_lng |
DOUBLE PRECISION | Par usado na resolução. Preservado para auditoria. |
event_id |
UUID | event_id do evento demanda.normalizada que originou esta resolução. Para rastreabilidade. |
correlacao_id |
UUID | correlacao_id do evento de origem. Para trace distribuído. Quando o evento não traz o campo, o demanda_id assume. |
criado_em |
TIMESTAMPTZ(2) | Timestamp de criação da resolução. |
2.3 Tabela d2.eventos_processados
Seção intitulada “2.3 Tabela d2.eventos_processados”Marca os eventos com caminho terminal no handler da D-2. A PK é o event_id, o que torna a gravação idempotente por construção.
CREATE TABLE d2.eventos_processados ( event_id UUID NOT NULL, demanda_id UUID NOT NULL, processado_em TIMESTAMPTZ(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT eventos_processados_pkey PRIMARY KEY (event_id));
CREATE INDEX eventos_processados_demanda_id_idx ON d2.eventos_processados (demanda_id);A D-2 grava a linha em todo caminho terminal: demanda georreferenciada, demanda sem coordenadas, coordenadas inválidas e ponto fora de todos os polígonos. A idempotência de reprocessamento consulta d2.resolucoes_geo; a tabela de eventos processados registra a passagem do evento pelos caminhos que não geram resolução.
2.4 Tabela d2.revisoes_geo_pendentes
Seção intitulada “2.4 Tabela d2.revisoes_geo_pendentes”Fila de investigação manual para demandas cujo ponto não está contido em nenhum polígono. Uma linha por demanda_id.
CREATE TABLE d2.revisoes_geo_pendentes ( demanda_id UUID NOT NULL, event_id UUID NOT NULL, coordenadas_lat DOUBLE PRECISION NOT NULL, coordenadas_lng DOUBLE PRECISION NOT NULL, criado_em TIMESTAMPTZ(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT revisoes_geo_pendentes_pkey PRIMARY KEY (demanda_id));
CREATE UNIQUE INDEX revisoes_geo_pendentes_event_id_key ON d2.revisoes_geo_pendentes (event_id);A D-2 insere a pendência, loga o aviso com o contador e avança o cursor. A resolução posterior depende da inclusão de novos polígonos em core.uc_polygons (mapeamento coletivo ou Fase 2).
2.5 Tabela d2.consumer_offset
Seção intitulada “2.5 Tabela d2.consumer_offset”Controla o cursor de processamento para replay seletivo após falha ou reinicialização. Segue o padrão definido pela N-0a (Event Bus).
CREATE TABLE d2.consumer_offset ( tipo_evento VARCHAR(255) PRIMARY KEY, last_sequence BIGINT NOT NULL DEFAULT 0, updated_at TIMESTAMPTZ(2) NOT NULL DEFAULT CURRENT_TIMESTAMP);No MVP, a tabela tem uma linha: tipo_evento = 'demanda.normalizada'.
2.6 Tabela core.uc_polygons (dependência de leitura do núcleo)
Seção intitulada “2.6 Tabela core.uc_polygons (dependência de leitura do núcleo)”A D-2 consulta esta tabela do schema core em modo somente leitura para realizar point-in-polygon. A mesma tabela é usada pela L-2. É uma simplificação de MVP (ver seção 1.6 e 8.2). A tabela é populada pelos scripts de seed territorial (seção 8.5) e nenhuma colônia de negócio escreve nela.
Estrutura de referência:
CREATE TABLE core.uc_polygons ( id UUID NOT NULL, unidade_civica_id UUID NOT NULL, nivel INTEGER NOT NULL, parent_uc_id UUID, nome VARCHAR(255) NOT NULL, nome_busca VARCHAR(255) NOT NULL DEFAULT '', geometria JSONB NOT NULL, -- GeoJSON Polygon ou MultiPolygon fonte VARCHAR(50) NOT NULL, metadados JSONB NOT NULL DEFAULT '{}', populacao_estimada INTEGER, criado_em TIMESTAMPTZ(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, atualizado_em TIMESTAMPTZ(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uc_polygons_pkey PRIMARY KEY (id));
CREATE INDEX uc_polygons_unidade_civica_id_idx ON core.uc_polygons (unidade_civica_id);CREATE INDEX uc_polygons_nivel_idx ON core.uc_polygons (nivel);As colunas fonte, metadados e populacao_estimada (migration 20260811195030_add_fonte_metadados_populacao_uc_polygons) são usadas pelos scripts de seed IBGE. populacao_estimada permanece NULL no MVP, porque nenhuma colônia consome população. A coluna nome_busca (migration 20260913120000_uc_polygons_nome_busca) mantém o nome em minúscula, sem acento e sem pontuação, por trigger, para a busca por nome na D-7.
O campo geometria armazena o polígono no formato GeoJSON. A escolha por JSONB em vez de PostGIS GEOMETRY é deliberada para o MVP: evita a instalação da extensão PostGIS e permite point-in-polygon via Turf.js. Na Fase 2, migra para GEOMETRY(POLYGON, 4326) com índice GIST e ST_Contains.
2.7 Migrations
Seção intitulada “2.7 Migrations”Migrations reais do schema d2 e da base territorial:
20260809112210_d2_uc_polygons_resolucoes_geo— Cria o schemad2, as tabelasd2.resolucoes_geoed2.eventos_processadose a tabelacore.uc_polygons(migration única compartilhada com o núcleo e a L-2).20260811195030_add_fonte_metadados_populacao_uc_polygons— Adicionafonte,metadadosepopulacao_estimadaacore.uc_polygons.20260813112406_add_consumer_offset_d1b_d2_d4— Criad2.consumer_offset(na mesma migration da D-1b e da D-4).20260814202321_create_d2_revisoes_geo_pendentes— Criad2.revisoes_geo_pendentes.20260913120000_uc_polygons_nome_busca— Adicionanome_buscae o trigger de normalização do nome.20260918140000_d2_eventos_processados_limpeza— Remove a coluna redundantecriado_emded2.eventos_processados.
Seed inicial do consumer_offset: no boot, offsetRepo.seed(['demanda.normalizada'], obterMaiorSequence()) cria a linha com a maior sequência do log sem sobrescrever cursor existente.
Migrations futuras (Fase 2): migração de geometria JSONB para geometry GEOMETRY(POLYGON, 4326) com PostGIS, substituição do CoreGeoRepository por réplica local sincronizada via evento, índices GIST espaciais.
2.8 Relações internas
Seção intitulada “2.8 Relações internas”Não há foreign keys entre as tabelas do schema d2. O demanda_id em d2.resolucoes_geo referencia o identificador de demanda em schemas de outras colônias (D-1a, D-1b), mas sem FK formal — regra de isolamento.
2.9 Decisões de schema
Seção intitulada “2.9 Decisões de schema”Tabela única d2.resolucoes_geo sem colunas de enriquecimento.
Diferente da L-2, a D-2 não faz enriquecimento de tipificação. A tabela contém apenas dados territoriais: UC resolvida, cadeia, método, confiança. A geocodificação de endereço textual (Fase 2) adicionará colunas como endereco_original e endereco_geocodificado sem alterar a estrutura fundamental.
coordenadas_lat e coordenadas_lng como DOUBLE PRECISION.
Duas colunas separadas expressam o par ordenado usado na resolução e são mais diretas de ler do que um array. Índices não são necessários, porque a consulta é sempre por demanda_id ou event_id, nunca por coordenadas.
confianca_geo sempre alta no MVP.
Todas as demandas chegam à D-2 com coordenadas GPS — a D-1a valida bounding box e a D-1b normaliza as coordenadas. O campo existe para forward compatibility com endereços textuais (Fase 2).
cadeia_ucs como UUID[] em vez de tabela de junção.
Mesmo racional da L-2: o array é compacto, a consulta é sempre “dado um lugar, quais UCs o contêm?” e a ordem é garantida.
d2.eventos_processados separado de d2.resolucoes_geo.
A idempotência de negócio vive na resolução, com unique em demanda_id. A marca de evento processado cobre também os descartes, que não geram resolução. As duas consultas têm propósitos distintos e não se sobrepõem.
3. Eventos — Contratos Detalhados
Seção intitulada “3. Eventos — Contratos Detalhados”A D-2 consome um tipo de evento e produz um. Os schemas completos (JSON Schema draft-2020-12) estão definidos no Registry (N-0b). Esta seção descreve os contratos do ponto de vista da D-2.
3.1 Evento consumido: demanda.normalizada
Seção intitulada “3.1 Evento consumido: demanda.normalizada”| Propriedade | Valor |
|---|---|
| Tipo | demanda.normalizada |
| Schema version | 1.2.0 (o catálogo mantém 1.0.0 e 1.1.0) |
| Produtor | D-1b (Normalização) |
| Consumidor | D-2 (esta colônia) |
| Descrição | Demanda com campos estruturados, coordenadas validadas e score de confiança da normalização. |
Payload esperado (conforme Registry N-0b):
interface DemandaNormalizadaPayload { demanda_id: string; titulo: string; descricao_limpa: string; tipo_midia_processada: string; coordenadas_validadas: { lat: number; lng: number; }; confianca_normalizacao: number; // 0-1 origem_normalizacao: string; // 'automatico' | 'hibrido' conteudo_suspeito: boolean; // termo da denylist encontrado entidades_extraidas?: { endereco?: string; cep?: string; nomeRua?: string; // camelCase no payload real }; idioma_detectado?: string; termos_suspeitos?: string[]; // presente quando conteudo_suspeito = true categoria_id?: string; subcategoria_id?: string;}A D-2 lê demanda_id e coordenadas_validadas. Os demais campos seguem no evento para os outros consumidores.
3.2 Evento produzido: demanda.georreferenciada
Seção intitulada “3.2 Evento produzido: demanda.georreferenciada”| Propriedade | Valor |
|---|---|
| Tipo | demanda.georreferenciada |
| Schema version | 1.0.0 |
| Produtor | D-2 (esta colônia) |
| Consumidores | D-3 (Categorização), D-4 (Priorização e Ranking), D-7 (Transparência) |
| Descrição | Demanda com UC resolvida e cadeia de UCs pai completa. |
Payload publicado (conforme Registry N-0b):
interface DemandaGeorreferenciadaPayload { demanda_id: string; unidade_civica_id: string; // UC de menor nível resolvida nivel_minimo_resolvido: number; // 1-7 cadeia_ucs: string[]; // [uc_nivel_N, ..., uc_nivel_7] metodo_resolucao: string; // 'gps' | 'endereco' | 'inferencia' confianca_geo: string; // 'alta' | 'media' | 'baixa' coordenadas_lat?: number; // opcionais no schema; publicados pela D-2 coordenadas_lng?: number;}3.3 Ordem de operações no handler
Seção intitulada “3.3 Ordem de operações no handler”O fluxo em D2Service.processarDemandaNormalizada() segue esta ordem exata:
1. Verificar idempotência por event_id → consultar d2.resolucoes_geo WHERE event_id = ? → se encontrado: republicar demanda.georreferenciada e avançar o cursor
2. Extrair coordenadas do payload → payload.coordenadas_validadas → se ausente: log.warn, registrar em d2.eventos_processados e avançar o cursor
3. Validar coordenadas (defesa em profundidade) → lat entre -90 e 90, lng entre -180 e 180 → se inválidas: log.error, registrar em d2.eventos_processados e avançar o cursor
4. Resolver território (point-in-polygon) → consultar cache geo: d2:geo:{lat}:{lng} → se cache hit: usar o resultado do cache → se cache miss: → carregar o snapshot de polígonos (TTL de 6 horas) → executar point-in-polygon do menor para o maior nível → ao encontrar a UC: resolver a cadeia de UCs pai → armazenar no cache geo → se nenhum polígono contiver o ponto: inserir em d2.revisoes_geo_pendentes, log.warn, registrar em d2.eventos_processados e avançar o cursor
5. Inserir a resolução em d2.resolucoes_geo
6. Publicar demanda.georreferenciada no barramento → se a publicação falhar: log.error e exceção relançada; o cursor não avança. O replay seguinte encontra a resolução pela idempotência e republica a saída.
7. Registrar o evento em d2.eventos_processados e atualizar o consumer offset3.4 Tratamento de erro e idempotência
Seção intitulada “3.4 Tratamento de erro e idempotência”| Cenário | Comportamento |
|---|---|
| Evento reentregue (replay DLQ) | Detectado por event_id em d2.resolucoes_geo. Registro existente encontrado. Republica demanda.georreferenciada para garantir entrega a consumidores que possam ter perdido o evento original. Cursor avança. |
| Coordenadas ausentes no payload | Log.warn. Evento descartado. Linha em d2.eventos_processados. Cursor avança. A D-1b deve normalizar coordenadas antes de publicar. Se ocorrer, é bug upstream. |
| Coordenadas inválidas (fora -90/+90, -180/+180) | Log.error. Evento descartado. Linha em d2.eventos_processados. Cursor avança. |
| Point-in-polygon não encontra UC (ponto fora de todos os polígonos) | Log.warn com o contador de pendências. Linha em d2.revisoes_geo_pendentes para investigação manual. Cursor avança sem publicação. |
INSERT falha (violação de unique em demanda_id) |
Log.error e exceção relançada. Indica que o mesmo demanda_id foi processado duas vezes com event_id diferente — cenário impossível. O erro é registrado na DLQ e o cursor não avança. |
Publish de demanda.georreferenciada falha |
Log.error. Registro existe em d2.resolucoes_geo. DLQ registrada. Cursor não avança. |
| Cache em memória perdido em reinicialização | Degradação graciosa: point-in-polygon repete resoluções no boot seguinte. Performance reduzida temporariamente, funcionalidade preservada. |
3.5 Decisões de design com justificativa
Seção intitulada “3.5 Decisões de design com justificativa”Persistir antes de publicar. Mesmo princípio de todas as colônias: o estado próprio é a memória do módulo. Se o barramento falhar após o INSERT, a resolução está salva e recuperável.
Republicar evento de saída no replay.
A D-2 é o terceiro elo do pipeline de demandas. Sem demanda.georreferenciada, D-3, D-4 e D-7 não conseguem operar. A republicação no replay garante entrega at-least-once. As colônias consumidoras devem ser idempotentes.
Geocodificação de endereço textual adiada para a Fase 2.
No MVP, a D-1a captura coordenadas GPS do dispositivo e a D-1b as valida e normaliza. O campo entidades_extraidas.endereco pode conter endereço textual extraído do texto, mas a D-2 não o processa no MVP — descarta o evento com log.warn se não houver coordenadas. A geocodificação (Nominatim/Photon) adiciona latência de rede e dependência de serviço externo que não se justificam para fechar o ciclo mínimo.
4. Lógica de Negócio — Algoritmos e Fluxos
Seção intitulada “4. Lógica de Negócio — Algoritmos e Fluxos”4.1 D2Service.processarDemandaNormalizada() — pseudocódigo
Seção intitulada “4.1 D2Service.processarDemandaNormalizada() — pseudocódigo”função processarDemandaNormalizada(evento):
payload = evento.payload
// 1. Idempotência por event_id (com republicação) existente = resolucaoGeoRepo.buscarPorEventId(evento.event_id) se existente não é null: logger.log("Evento já processado, republicando saída", { event_id: evento.event_id, demanda_id: existente.demanda_id, }) await this.publicarDemandaGeorreferenciada(existente, evento.correlacao_id) await offsetRepo.upsert('demanda.normalizada', evento.sequence_number) retornar
// 2. Extrair coordenadas coordenadas = payload.coordenadas_validadas se coordenadas ausente: logger.warn("Demanda sem coordenadas — descartando", { event_id: evento.event_id, demanda_id: payload.demanda_id, }) await eventoProcessadoRepo.registrar(evento.event_id, payload.demanda_id) await offsetRepo.upsert('demanda.normalizada', evento.sequence_number) retornar
lat = coordenadas.lat lng = coordenadas.lng
// 3. Validar coordenadas (defesa em profundidade) se lat < -90 ou lat > 90 ou lng < -180 ou lng > 180: logger.error("Coordenadas inválidas", { event_id: evento.event_id, demanda_id: payload.demanda_id, lat, lng, }) await eventoProcessadoRepo.registrar(evento.event_id, payload.demanda_id) await offsetRepo.upsert('demanda.normalizada', evento.sequence_number) retornar
// 4. Resolver território resultado = geoCache.obter(lat, lng) se resultado é null: poligonos = await coreGeoRepo.buscarTodosOrdenadosPorNivel() resultado = pointInPolygonService.resolverTerritorio(lat, lng, poligonos)
se resultado é null: totalRevisoes = await resolucaoGeoRepo.contarRevisoesPendentes() await resolucaoGeoRepo.inserirRevisaoPendente({ demanda_id: payload.demanda_id, event_id: evento.event_id, coordenadas_lat: lat, coordenadas_lng: lng, }) logger.warn("Ponto fora de todos os polígonos — demanda marcada para revisão manual", { event_id: evento.event_id, demanda_id: payload.demanda_id, revisoes_pendentes: totalRevisoes + 1, }) await eventoProcessadoRepo.registrar(evento.event_id, payload.demanda_id) await offsetRepo.upsert('demanda.normalizada', evento.sequence_number) retornar
geoCache.definir(lat, lng, resultado)
// 5. Persistir resolucao = await resolucaoGeoRepo.inserir({ demanda_id: payload.demanda_id, unidade_civica_id: resultado.unidade_civica_id, nivel_minimo_resolvido: resultado.nivel_minimo_resolvido, cadeia_ucs: resultado.cadeia_ucs, metodo_resolucao: 'gps', confianca_geo: 'alta', coordenadas_lat: lat, coordenadas_lng: lng, event_id: evento.event_id, correlacao_id: evento.correlacao_id ?? payload.demanda_id, })
// 6. Publicar await this.publicarDemandaGeorreferenciada(resolucao, evento.correlacao_id)
// 7. Fechar o ciclo await eventoProcessadoRepo.registrar(evento.event_id, payload.demanda_id) await offsetRepo.upsert('demanda.normalizada', evento.sequence_number)4.2 resolverTerritorio() — point-in-polygon com cache
Seção intitulada “4.2 resolverTerritorio() — point-in-polygon com cache”A busca no cache e o point-in-polygon vivem em pontos distintos do módulo. O D2Service consulta o GeoCacheService e, no miss, carrega o snapshot pelo CoreGeoRepository e delega o cálculo ao PointInPolygonService.
função no D2Service (trecho da resolução):
cacheHit = geoCache.obter(lat, lng) se cacheHit não é null: resultado = cacheHit senão: poligonos = await coreGeoRepo.buscarTodosOrdenadosPorNivel() resultado = pointInPolygonService.resolverTerritorio(lat, lng, poligonos) se resultado é null: // seção 3.3, passo 4: revisão pendente geoCache.definir(lat, lng, resultado)
função resolverTerritorio(lat, lng, poligonos) -> ResultadoTerritorio | null:
ponto = turf.point([lng, lat])
para cada poligono em poligonos: se pontoDentroDoBbox(poligono.bbox, lat, lng) é falso: continuar tentar: contido = turf.booleanPointInPolygon(ponto, poligono.geometria) se contido: return { unidade_civica_id: poligono.unidade_civica_id, nivel_minimo_resolvido: poligono.nivel, cadeia_ucs: resolverCadeiaUcs(poligono, poligonos), } capturar erro: logger.warn("Polígono inválido ignorado: uc_id=...")
return nullO snapshot de polígonos é carregado ordenado por nível e cada polígono leva o próprio bounding box. O PointInPolygonService descarta pelo bbox antes de chamar o Turf. O cache geo usa o prefixo d2:geo: e arredondamento de 5 casas decimais.
4.3 resolverCadeiaUcs() — subida na hierarquia
Seção intitulada “4.3 resolverCadeiaUcs() — subida na hierarquia”A cadeia é resolvida na lista de polígonos já carregada, sem consulta por nível.
função resolverCadeiaUcs(uc: UcPolygon, todosPoligonos: UcPolygon[]) -> UUID[]:
cadeia = [uc.unidade_civica_id] parentId = uc.parent_uc_id
enquanto parentId não é null: pai = todosPoligonos.find(p => p.unidade_civica_id === parentId) se pai é undefined: break cadeia.push(pai.unidade_civica_id) parentId = pai.parent_uc_id
return cadeia4.4 D2Service.iniciar() — protocolo de inicialização
Seção intitulada “4.4 D2Service.iniciar() — protocolo de inicialização”função iniciar(): se já iniciado: retornar
// 1. Iniciar o cache geo geoCache.iniciar()
// 2. Seed do cursor na maior sequência do log (sem sobrescrever) maiorSequence = await eventBus.obterMaiorSequence() await offsetRepo.seed(['demanda.normalizada'], maiorSequence)
// 3. Replay de eventos perdidos durante inatividade await this.reprocessarEventosPerdidos() // lê o cursor em consumer_offset, chama replayDeSequence(cursor, ['demanda.normalizada']) // e despacha cada evento pelo handler; falha por evento é logada e o evento // permanece pendente para o próximo boot
// 4. Registrar handler para eventos futuros this.registrarConsumidores() // eventBus.inscrever('demanda.normalizada', 'D-2', this.processarDemandaNormalizada.bind(this))
logger.log("D-2 Georreferenciamento inicializado")4.5 Casos de borda
Seção intitulada “4.5 Casos de borda”| Caso | Comportamento |
|---|---|
Demanda com coordenadas_validadas = null (apenas endereço textual) |
MVP descarta com log.warn. Fase 2: geocodifica e prossegue. |
| Ponto fora de todos os polígonos (área não coberta) | resolverTerritorio() retorna null. Linha em d2.revisoes_geo_pendentes com log.warn (não publicado). |
| Polígono com geometria inválida | Turf.js lança erro. Capturado, log.warn com uc_id. Polígono pulado. |
Hierarquia de UCs quebrada (parent_uc_id sem correspondência) |
resolverCadeiaUcs() interrompe a subida e retorna a cadeia parcial. |
| Cache em memória perdido em reinicialização | Reconstrução natural: resoluções repetidas no boot. Funcionalidade preservada. |
Dois handlers concorrentes processando o mesmo event_id |
Impossível no MVP monolito. A PK de d2.eventos_processados e o unique de d2.resolucoes_geo em demanda_id garantem a idempotência mesmo se ocorrer. |
4.6 Decisões de design com justificativa
Seção intitulada “4.6 Decisões de design com justificativa”Point-in-polygon com Turf.js, não PostGIS. Mesmo racional da L-2: Turf.js é maduro, testado e não exige extensão PostgreSQL. Para o volume do MVP, a performance é adequada. A migração para PostGIS na Fase 2 é documentada.
Cache geo compartilhando padrão com a L-2, mas com prefixo independente.
D-2 e L-2 usam o mesmo algoritmo e a mesma tabela core.uc_polygons, mas mantêm caches independentes (d2:geo:* vs. l2:geo:*). Isso preserva isolamento: cada colônia gerencia seu próprio ciclo de vida de cache (TTL, invalidação, métricas). A duplicação de cache é inofensiva — algumas dezenas de KB por colônia.
Sem enriquecimento de tipificação. A D-2 resolve apenas território. A tipificação (categorização do conteúdo) é responsabilidade da D-3 (Categorização), que opera sobre o texto da demanda, não sobre coordenadas. A L-2 faz enriquecimento de tipificação porque lugares têm tipo (farmácia, escola) associado à localização — demandas não.
5. Integração com o Barramento e Outras Colônias
Seção intitulada “5. Integração com o Barramento e Outras Colônias”5.1 Consumo de eventos via EventBusService
Seção intitulada “5.1 Consumo de eventos via EventBusService”A D-2 injeta EventBusService (do módulo @Global() N-0a) para três operações: inscrever() (registrar handler para demanda.normalizada), publicar() (publicar demanda.georreferenciada) e replayDeSequence() (replay na inicialização).
@Injectable()export class D2Service { private readonly logger = new Logger(D2Service.name);
constructor( private readonly eventBus: EventBusService, private readonly resolucaoGeoRepo: ResolucaoGeoRepository, private readonly eventoProcessadoRepo: EventoProcessadoRepository, private readonly offsetRepo: ConsumerOffsetRepository, private readonly coreGeoRepo: CoreGeoRepository, private readonly pointInPolygonService: PointInPolygonService, private readonly geoCache: GeoCacheService, ) {}}O logging usa o Logger do NestJS. A D-2 não injeta ObservabilityService nem MetricsService da N-0c. Nenhuma dependência além do núcleo e do CoreGeoRepository (simplificação MVP).
5.2 Fluxo de eventos — cadeia completa
Seção intitulada “5.2 Fluxo de eventos — cadeia completa”Cidadão (app) → POST /demandas (BFF D-1a) → demanda.recebida (barramento) → [D-1b] → demanda.normalizada → [D-2] — esta colônia → demanda.georreferenciada → [D-3] → demanda.categorizada → [D-4] → demanda.ranqueada (usa UC para posicionar no ranking) → [D-7] → timeline + dashboard (usa UC para agregação territorial)A D-2 é o terceiro elo do pipeline. Sem seu evento de saída, a D-3 não consegue categorizar (precisa da UC para contexto territorial), a D-4 não consegue ranquear (cada UC tem seu próprio ranking) e a D-7 não consegue agregar territorialmente.
5.3 Publicação de demanda.georreferenciada
Seção intitulada “5.3 Publicação de demanda.georreferenciada”private async publicarDemandaGeorreferenciada( demandaId: string, unidadeCivicaId: string, nivelMinimoResolvido: number, cadeiaUcs: string[], metodoResolucao: string, confiancaGeo: string, coordenadasLat: number, coordenadasLng: number, correlacaoId: string | null,): Promise<void> { try { const novoEventId = uuidv4();
await publicarComRetry(this.eventBus, { tipo: 'demanda.georreferenciada', origem: 'D-2', event_id: novoEventId, correlacao_id: correlacaoId ?? demandaId, payload: { demanda_id: demandaId, unidade_civica_id: unidadeCivicaId, nivel_minimo_resolvido: nivelMinimoResolvido, cadeia_ucs: cadeiaUcs, metodo_resolucao: metodoResolucao, confianca_geo: confiancaGeo, coordenadas_lat: coordenadasLat, coordenadas_lng: coordenadasLng, }, }); } catch (erro: unknown) { this.logger.error('Falha ao publicar demanda.georreferenciada', { demanda_id: demandaId, erro: erro instanceof Error ? erro.message : String(erro), }); throw erro; }}O versao_schema é omitido e o Event Bus publica a versão 1.0.0, padrão do tipo. A publicação usa publicarComRetry (4 tentativas, backoff de 500 ms), como o protocolo do AGENTS.md exige: a resolução já está persistida e o replay do próximo boot republica a saída pendente.
5.4 Chamadas síncronas via BFF
Seção intitulada “5.4 Chamadas síncronas via BFF”A D-2 não faz chamadas síncronas a outras colônias. Não expõe endpoints REST. Não atua como proxy. Toda comunicação é via barramento.
5.5 Dependências de projeções de leitura
Seção intitulada “5.5 Dependências de projeções de leitura”A D-2 não consome projeções de leitura de outras colônias. Os dados externos que acessa são:
core.event_logviaEventBusService.replayDeSequence()— dependência do núcleo, permitida.core.uc_polygonsviaCoreGeoRepository— simplificação MVP, será removida na Fase 2.
5.6 Cache geo em memória (sem Redis)
Seção intitulada “5.6 Cache geo em memória (sem Redis)”A D-2 usa Map em memória para cache de resoluções geo, com chave d2:geo:{lat}:{lng} (coordenadas arredondadas a 5 casas decimais):
| Chave | Conteúdo | TTL |
|---|---|---|
d2:geo:{lat}:{lng} |
{ unidade_civica_id, nivel_minimo_resolvido, cadeia_ucs } |
30 dias (limpeza a cada hora por setInterval) |
O snapshot de polígonos do CoreGeoRepository é um segundo cache em memória, com TTL de 6 horas e recarga automática na primeira consulta após o vencimento.
Sem Redis no MVP — o cache não sobrevive a reinicializações e é simplesmente repopulado por novas resoluções. Redis com prefixo d2:geo:* entra na Fase 2, com múltiplas instâncias do monolito.
6. Performance e Limites
Seção intitulada “6. Performance e Limites”6.1 Rate limiting
Seção intitulada “6.1 Rate limiting”A D-2 não aplica rate limiting. O volume de eventos é limitado indiretamente pelo rate limiting do BFF.
6.2 Cotas e limites de tamanho
Seção intitulada “6.2 Cotas e limites de tamanho”| Limite | Valor | Justificativa |
|---|---|---|
| Arredondamento cache geo | 5 casas decimais | ~1.1m de precisão. Idêntico ao padrão da L-2. |
| Cache geo TTL | 30 dias (2.592.000s) | Polígonos de UC mudam a cada censo (~10 anos). |
| Snapshot de polígonos TTL | 6 horas (21.600.000ms) | Recarga automática após novo seed, sem reiniciar a API. |
| Polígonos carregados em memória | ~5.600 | Pior caso: todos os municípios brasileiros. ~17 MB em memória. |
6.3 Índices e padrões de query
Seção intitulada “6.3 Índices e padrões de query”| Índice | Query atendida |
|---|---|
resolucoes_geo_demanda_id_key (demanda_id, UNIQUE) |
Deduplicação de negócio. Acesso por demanda_id. |
resolucoes_geo_event_id_idx (event_id) |
Idempotência — toda invocação do handler. |
resolucoes_geo_unidade_civica_id_idx (unidade_civica_id) |
“Demandas nesta UC” (dashboard D-7, ranking D-4). |
resolucoes_geo_confianca_geo_idx (confianca_geo) |
Dashboard: demandas com localização incerta. |
eventos_processados_pkey (event_id) |
Idempotência de descarte. |
revisoes_geo_pendentes_pkey (demanda_id) e revisoes_geo_pendentes_event_id_key (event_id) |
Fila de investigação manual. |
uc_polygons_unidade_civica_id_idx (unidade_civica_id) |
resolverCadeiaUcs() na L-2 e buscas por UC. |
uc_polygons_nivel_idx (nivel) |
Carga de polígonos ordenados. |
6.4 Padrões de query esperados
Seção intitulada “6.4 Padrões de query esperados”buscarPorEventId(): 1 query por evento recebido (idempotência).buscarTodosOrdenadosPorNivel(): 1 query a cada 6 horas (snapshot de polígonos), não por evento.inserir(): 1 insert por demanda resolvida.inserirRevisaoPendente()econtarRevisoesPendentes(): apenas no caminho de ponto fora de todos os polígonos.resolverCadeiaUcs(): resolvida em memória sobre o snapshot, sem query por nível.
Volume esperado no MVP: < 100 demandas/dia. Tempo médio de processamento: < 10ms com cache geo hit, < 50ms com cache miss.
6.5 Estratégia de cache
Seção intitulada “6.5 Estratégia de cache”- Cache geo em memória:
Map<string, EntradaCache>com TTL de 30 dias e limpeza periódica porsetIntervalde uma hora. - Snapshot de polígonos:
CoreGeoRepositoryguarda a lista ordenada por nível comcarregadoEm, invalida após 6 horas e expõeinvalidarCache()para recarga manual.
A D-2 é mais simples que a L-2 — não tem cache OSM nem enriquecimento externo. Os únicos caches são o geo e o snapshot de polígonos.
6.6 Projeção de volume
Seção intitulada “6.6 Projeção de volume”| Cenário | Demandas/dia | Point-in-polygon/dia (cache miss) |
|---|---|---|
| PoC (1 bairro, 10 entusiastas) | ~20 | ~10 |
| MVP (1 município, centenas de usuários) | ~100 | ~40 |
| Fase 2 (regional) | ~10.000 | ~3.000 |
7. Testabilidade
Seção intitulada “7. Testabilidade”7.1 Como testar o módulo isolado
Seção intitulada “7.1 Como testar o módulo isolado”Teste unitário do D2Service:
beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ D2Service, { provide: EventBusService, useValue: mockEventBus }, { provide: ResolucaoGeoRepository, useValue: mockResolucaoGeoRepo }, { provide: EventoProcessadoRepository, useValue: mockEventoProcessadoRepo }, { provide: ConsumerOffsetRepository, useValue: mockOffsetRepo }, { provide: CoreGeoRepository, useValue: mockCoreGeoRepo }, { provide: PointInPolygonService, useValue: mockPointInPolygonService }, { provide: GeoCacheService, useValue: mockGeoCache }, ], }).compile();
service = module.get(D2Service);
mockEventBus.inscrever = jest.fn(); mockEventBus.publicar = jest.fn(); mockEventBus.replayDeSequence.mockResolvedValue([]); mockEventBus.obterMaiorSequence.mockResolvedValue(100);
mockResolucaoGeoRepo.buscarPorEventId.mockResolvedValue(null); mockResolucaoGeoRepo.inserir.mockResolvedValue({ /* registro persistido */ }); mockResolucaoGeoRepo.inserirRevisaoPendente.mockResolvedValue(undefined); mockResolucaoGeoRepo.contarRevisoesPendentes.mockResolvedValue(0);
mockEventoProcessadoRepo.registrar.mockResolvedValue(undefined); mockOffsetRepo.seed.mockResolvedValue(undefined); mockOffsetRepo.buscarTodos.mockResolvedValue([]); mockOffsetRepo.upsert.mockResolvedValue(undefined);
mockGeoCache.obter.mockReturnValue(null); mockGeoCache.definir = jest.fn();
mockCoreGeoRepo.buscarTodosOrdenadosPorNivel.mockResolvedValue([ // polígonos de exemplo com bbox calculado ]);
mockPointInPolygonService.resolverTerritorio.mockReturnValue({ unidade_civica_id: 'uc-municipio', nivel_minimo_resolvido: 4, cadeia_ucs: ['uc-municipio', 'uc-estado', 'uc-brasil'], });});7.2 Cenários de teste críticos
Seção intitulada “7.2 Cenários de teste críticos”Happy path:
| # | Cenário | Verificação |
|---|---|---|
| T1 | processarDemandaNormalizada() com coordenadas válidas, UC resolvida |
INSERT em d2.resolucoes_geo com confianca_geo = 'alta'. publicar() chamado com demanda.georreferenciada. Consumer offset atualizado. |
| T2 | iniciar() sem eventos perdidos |
seed() chamado com obterMaiorSequence(). replayDeSequence() chamado com o cursor. inscrever() registrado. |
| T3 | iniciar() com eventos perdidos |
replayDeSequence() retorna 3 eventos. processarDemandaNormalizada() chamado 3 vezes. |
| T4 | Cache geo hit — coordenadas já resolvidas | geoCache.obter() retorna resultado. Nenhum point-in-polygon executado. CoreGeoRepository não chamado. |
Falhas e bordas:
| # | Cenário | Verificação |
|---|---|---|
| T5 | Evento já processado (mesmo event_id) — com republicação |
buscarPorEventId() retorna registro. demanda.georreferenciada republicado. |
| T6 | Coordenadas ausentes (coordenadas_validadas = null) |
Log.warn. Registrar em d2.eventos_processados. Handler retorna sem INSERT. |
| T7 | Coordenadas inválidas (lat: 200) | Log.error. Registrar em d2.eventos_processados. Handler retorna sem INSERT. |
| T8 | Point-in-polygon não encontra UC | resolverTerritorio() retorna null. Linha em d2.revisoes_geo_pendentes. Log.warn. Sem INSERT de resolução. |
| T9 | publicar() de demanda.georreferenciada falha |
Registro persiste. Erro relançado para DLQ. Consumer offset NÃO atualizado. |
| T10 | Reinicialização — cache geo vazio | geoCache.iniciar() recria o Map. Resoluções repetem point-in-polygon até o cache repopular. Funcionalidade preservada. |
Teste de integração:
| # | Cenário | Verificação |
|---|---|---|
| T11 | Ciclo completo: demanda.normalizada → demanda.georreferenciada |
1 linha em d2.resolucoes_geo. 1 evento em core.event_log tipo demanda.georreferenciada. correlacao_id propagado. |
| T12 | Replay após reinício: 3 eventos, 1 já processado | iniciar() processa 2 novos, república 1. count(*) = 3. 3 eventos publicados. |
| T13 | Duas demandas na mesma coordenada | Primeira: point-in-polygon + cache. Segunda: cache hit. Ambas georreferenciadas com mesma UC. |
| T14 | Demanda em coordenada que cruza com UC de nível 4 (sem níveis 1-3 definidos) | nivel_minimo_resolvido = 4. cadeia_ucs começa no nível 4. |
7.3 Dados de seed para desenvolvimento local
Seção intitulada “7.3 Dados de seed para desenvolvimento local”O seed real do ambiente de desenvolvimento é o npx prisma db seed, que importa a malha municipal 2025 e a malha de bairros do Censo 2022 (seção 8.5). O SQL abaixo é ilustrativo de um cenário mínimo.
-- Resolução de exemplo (o cursor do consumer_offset é semeado no boot)INSERT INTO d2.resolucoes_geo (id, demanda_id, unidade_civica_id, nivel_minimo_resolvido, cadeia_ucs, metodo_resolucao, confianca_geo, coordenadas_lat, coordenadas_lng, event_id, correlacao_id)VALUES ( 'f6a7b8c9-d0e1-2345-fabc-678901abcdef', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'uc-jardim-flores', 2, ARRAY['uc-jardim-flores', 'uc-municipio-sp', 'uc-estado-sp', 'uc-brasil'], 'gps', 'alta', -23.552, -46.634, 'b2c3d4e5-f6a7-8901-bcde-f12345678901', 'b2c3d4e5-f6a7-8901-bcde-f12345678901' );8. Alinhamento com o MVP
Seção intitulada “8. Alinhamento com o MVP”8.1 O que é MVP obrigatório
Seção intitulada “8.1 O que é MVP obrigatório”| Funcionalidade | Status |
|---|---|
inscrever('demanda.normalizada') com handler idempotente por event_id |
MVP obrigatório |
| Extração de coordenadas do payload com validação de intervalo | MVP obrigatório |
Point-in-polygon via Turf.js contra core.uc_polygons |
MVP obrigatório |
| Resolução de UC de menor nível com cadeia completa de UCs pai | MVP obrigatório |
| Cache geo de resoluções (in-memory) | MVP obrigatório |
| Snapshot de polígonos com TTL e pré-filtro por bounding box | MVP obrigatório |
Persistência em d2.resolucoes_geo |
MVP obrigatório |
Publicação de demanda.georreferenciada com payload conforme Registry |
MVP obrigatório |
| Republicação no replay para garantir entrega a D-3, D-4 e D-7 | MVP obrigatório |
| Consumer offset para replay após falha | MVP obrigatório |
Propagação de correlacao_id |
MVP obrigatório |
Fila de revisão manual para pontos fora de todos os polígonos (d2.revisoes_geo_pendentes) |
MVP obrigatório |
Logs estruturados com demanda_id, event_id, correlacao_id e UC resolvida |
MVP obrigatório |
8.2 Simplificações válidas no MVP
Seção intitulada “8.2 Simplificações válidas no MVP”| Simplificação | Justificativa | Quando remover |
|---|---|---|
CoreGeoRepository com leitura direta de core.uc_polygons |
Duplicar a base de polígonos em D-2 e L-2 no MVP geraria complexidade de sincronização sem benefício. A base no core é uma dependência de infraestrutura compartilhada com a L-2. |
Migrar para colônia dedicada de geo na Fase 2. D-2 e L-2 mantêm réplicas locais sincronizadas por evento. |
| Polígono como GeoJSON em JSONB, sem PostGIS | Turf.js resolve point-in-polygon com performance adequada. PostGIS exigiria extensão PostgreSQL. | Migrar para GEOMETRY(POLYGON, 4326) + ST_Contains + índice GIST na Fase 2. |
| Polígonos carregados integralmente em memória, com TTL de 6 horas | ~5.600 polígonos ~= 17 MB. A recarga periódica dispensa reiniciar a API após novo seed. | Migrar para queries espaciais indexadas com > 100K polígonos. |
| Sem geocodificação de endereço textual | No MVP, a D-1a captura GPS e a D-1b normaliza coordenadas. Demandas sem coordenadas são descartadas com log.warn. | Adicionar geocodificação (Nominatim/Photon) na Fase 2. |
metodo_resolucao sempre gps, confianca_geo sempre alta |
Consequência da simplificação anterior. Campos existem para forward compatibility. | Passam a variar quando geocodificação textual for suportada. |
| Sem endpoint REST para consulta | A consulta é feita pela D-7 (Transparência) via projeções de leitura. | Adicionar endpoint administrativo se necessário na Fase 2. |
8.3 O que vai para a Fase 2
Seção intitulada “8.3 O que vai para a Fase 2”- Migração de
core.uc_polygonspara colônia dedicada de infraestrutura geoespacial, com réplicas locais em D-2 e L-2 - PostGIS:
GEOMETRY(POLYGON, 4326), índice GIST,ST_Contains - Geocodificação de endereço textual via Nominatim/Photon (self-hosted)
metodo_resolucao = 'endereco'econfianca_geo = 'media'para demandas geocodificadasmetodo_resolucao = 'inferencia'econfianca_geo = 'baixa'para fallback por centróide- Métricas Prometheus:
d2_demandas_georreferenciadas_total,d2_point_in_polygon_duration_ms,d2_cache_hit_ratio - Re-georreferenciamento quando polígonos de UC são atualizados
- Tratamento automático (ou ferramenta dedicada) das revisões pendentes de geo
8.4 Verificação de conflitos com outras colônias
Seção intitulada “8.4 Verificação de conflitos com outras colônias”Conflito potencial: D-2 e L-2 compartilham core.uc_polygons — divergência de algoritmo?
Avaliação: ambas usam o mesmo algoritmo (ray casting via Turf.js, mesma versão) sobre a mesma tabela. O resultado é determinístico: mesma coordenada → mesma UC em ambas as colônias. Teste de regressão cruzada deve verificar isso. Na Fase 2, ST_Contains do PostGIS é o algoritmo canônico único.
Conflito potencial: D-2 e L-2 mantêm caches independentes (d2:geo:* vs. l2:geo:*) com os mesmos dados?
Avaliação: sim, há duplicação de cache entre as colônias. É intencional: cada colônia gerencia seu próprio ciclo de vida de cache (TTL, invalidação, métricas) e o isolamento é preservado. A duplicação é inofensiva — algumas dezenas de KB. Se o volume de coordenadas únicas crescer a ponto de ser relevante (Fase 2), o cache pode ser unificado em uma camada de infraestrutura compartilhada.
Conflito potencial: D-1b publica demanda.normalizada com coordenadas que falharam na validação da D-2?
Avaliação: a D-1b já valida bounding box e intervalo de coordenadas. Se coordenadas inválidas chegarem à D-2, é bug na D-1b. A D-2 mantém validação própria (defesa em profundidade) e descarta com log.error. Sem conflito — a D-2 é a última linha de defesa.
Conflito potencial: a D-3 (Categorização) depende de demanda.georreferenciada mas também consome demanda.normalizada. Se a D-2 falhar, a D-3 fica bloqueada?
Avaliação: a D-3 deve consumir demanda.georreferenciada e usar demanda.normalizada apenas como dado complementar. Se a D-2 publicar com atraso (ex: replay após falha), a D-3 processa quando o evento chegar. O pipeline é assíncrono — a ordem de chegada dos eventos é garantida pelo sequence number, mas a D-3 não deve assumir que georreferenciada chega imediatamente após normalizada. Isso é responsabilidade da D-3, não da D-2. Sem conflito para a D-2.
8.5 Estratégia de seed da base territorial (core.uc_polygons)
Seção intitulada “8.5 Estratégia de seed da base territorial (core.uc_polygons)”A tabela core.uc_polygons é a fundação geoespacial sobre a qual todas as colônias de demandas e lugares operam. Sem polígonos populados, nenhum point-in-polygon funciona e o pipeline de georreferenciamento retorna null para todas as coordenadas. A população inicial dessa tabela é, portanto, o primeiro passo operacional antes de qualquer ciclo de demanda.
Abordagem em três camadas — da mais automatizada para a mais comunitária:
Camada 1 — Importação automatizada (IBGE)
Seção intitulada “Camada 1 — Importação automatizada (IBGE)”Fonte para os níveis 2 (bairros) e 4 a 7 (municípios, UFs, regiões e país). Os shapefiles do IBGE são abertos, gratuitos e cobrem todo o território nacional.
- Malhas: malha municipal 2025 (
BR_Municipios_2025,BR_UF_2025,BR_Regioes_2025,BR_Pais_2025) e malha de bairros do Censo Demográfico 2022 (BR_bairros_CD2022), em geoftp.ibge.gov.br - Conversão: implementada em Node com
shapefilee@turf/turf(scripts/seed-ibge/importar.ts), com encoding UTF-8 explícito e SIRGAS 2000 tratado como WGS84 - Hierarquia: os bairros trazem o
CD_MUNe o pai é resolvido pelo código oficial contra os municípios já importados. Fontes sem código, como o OSM, dependem de point-in-polygon - Idempotência: chave por
codigo_municipio,codigo_uf,codigo_regiaoecodigo_bairronos metadados - Simplificação: por arquivo. 0,001 grau (cerca de 110 m) nos níveis macro e 0,0001 (cerca de 11 m) nos bairros. Nos bairros, quando a geometria simplificada falha na validação, a original é usada. Nos níveis macro, geometria inválida é descartada e o descarte aparece no resumo do importador
- Volume: 17.575 bairros, 5.519 municípios, 25 UFs e 4 regiões, cerca de 66 MB em JSONB
A cobertura de bairros é parcial por natureza. Bairro é competência municipal: o IBGE representa apenas os legalmente instituídos, aproximados por setores censitários, em 895 municípios e 25 UFs, sem Tocantins e sem o Distrito Federal. Bairros cujo município não está no banco entram com parent_uc_id nulo; são 306, todos em municípios descartados pela simplificação municipal. Onde não há bairro, o sistema opera na granularidade disponível. Os níveis 1 (setores e micro-áreas) e as lacunas do nível 2 seguem para o OSM e o mapeamento coletivo.
Desempenho da resolução com os bairros: a D-2 e a L-2 mantêm um snapshot em memória da lista de polígonos (CoreGeoRepository, TTL de 6 horas) e cada polígono carrega o próprio bounding box. O PointInPolygonService descarta pelo bbox antes de chamar o Turf. Com 5.549 polígonos, a resolução caiu de até 7,99 ms para a faixa de 0,005 a 0,090 ms por ponto. A recarga do snapshot acontece na primeira consulta após o TTL, sem reiniciar a API.
Camada 2 — Mapeamento coletivo (cidadãos desenham polígonos)
Seção intitulada “Camada 2 — Mapeamento coletivo (cidadãos desenham polígonos)”O nível 1 (micro-unidade cívica / setor) e o nível 2 (bairro) são populados por cidadãos desenhando os contornos no mapa. Este é o mecanismo alinhado com a filosofia do projeto — a mesma lógica de “cidadão reporta demanda” se aplica a “cidadão define o território”.
Fluxo:
Cidadão (front-end) → Abre ferramenta de desenho no mapa (Leaflet.Draw / react-leaflet-draw) → Desenha polígono fechado do bairro/setor → Submete como lugar com tipo_lugar = 'poligono_uc' → D-1a publica lugar.recebido no barramento
L-1 (Cadastro de Lugares) → Valida geometria: anel fechado, sem auto-intersecção, área > mínima → Deduplica com polígonos existentes da mesma UC (sobreposição > limiar = alerta) → Gera lugar_id e publica lugar.cadastrado
L-2 (Georreferenciamento e Tipificação) → Identifica tipo_lugar = 'poligono_uc' → Loga "capturado e mantido em análise" e avança o cursor sem georreferenciar nem publicar (a inserção em core.uc_polygons é Fase 2)
Colônia de Infraestrutura Geoespacial (Fase 2 — Núcleo) → MVP: o seed manual direto em core.uc_polygons supre essa lacuna → Fase 2: colônia dedicada consome lugar.cadastrado com tipo_lugar = 'poligono_uc' → Valida contra polígonos existentes → Se sobreposição com polígono existente > 80%: ignora (já mapeado) → Se sobreposição entre 50-80%: cria versão alternativa, sinaliza para revisão → Se sem sobreposição significativa: insere como novo polígono em core.uc_polygons → Publica uc.poligono_adicionado no barramentoA validação fina da geometria pertence à L-1 no MVP e à colônia de infraestrutura na Fase 2. O caminho de captura do polígono fica inerte na L-2 até a Fase 2, quando a inserção em core.uc_polygons passa a ser automática.
Validação implícita por uso: Quanto mais demandas e lugares são georreferenciados dentro de um polígono desenhado pela comunidade, mais validação implícita ele recebe. Se o polígono estiver errado (ex: deslocado 500m do lugar real), as demandas daquela área cairão no polígono errado ou fora de todos os polígonos — o sistema acumulará evidência de que a geometria precisa de revisão.
Resolução de conflitos entre desenhos: Múltiplos cidadãos podem desenhar o mesmo bairro gerando polígonos ligeiramente diferentes. A heurística de consenso é simples: o polígono com maior área de intersecção com os demais vira o canônico. Versões alternativas são preservadas como histórico. A cada N submissões conflitantes, um alerta de moderação é gerado.
Camada 3 — OpenStreetMap como fallback (nível 2 apenas, planejada)
Seção intitulada “Camada 3 — OpenStreetMap como fallback (nível 2 apenas, planejada)”Para bairros que já estão mapeados no OSM (relação admin_level=10), a Overpass API pode ser usada para importação automatizada do nível 2. A cobertura no Brasil é incompleta mas crescente — capitais e regiões metropolitanas tendem a ter bairros mapeados.
- Query:
rel["admin_level"="10"]["name"](area);sobre o município - Volume estimado: ~20% dos municípios brasileiros têm bairros mapeados no OSM
- Quando usar: Como complemento à importação IBGE, antes de liberar o mapeamento coletivo. Reduz o esforço manual nas áreas já cobertas
Ordem de execução do seed no MVP
Seção intitulada “Ordem de execução do seed no MVP”1. Rodar o seed IBGE (malha municipal 2025 e bairros do Censo 2022) → 23.123 linhas em core.uc_polygons2. Rodar script de importação OSM (nível 2, bairros disponíveis) → completa lacunas onde o IBGE não tem bairro3. Liberar ferramenta de desenho no front-end (nível 1 e bairros restantes)4. Cidadãos desenham os polígonos faltantes5. Sistema opera com os polígonos disponíveis; áreas sem bairro caem no município (nível 4) ou na granularidade superior disponívelOnde não há polígono de bairro, o nivel_minimo_resolvido retorna o nível disponível, tipicamente 4 (município). O sistema não bloqueia — opera com a granularidade disponível e melhora conforme o território é mapeado.
Seed do MVP — referência de implementação
Seção intitulada “Seed do MVP — referência de implementação”O seed roda por npx prisma db seed, registrado em prisma.config.ts e orquestrado por prisma/seed-ibge.ts:
scripts/seed-ibge/baixar.shbaixa e extrai os shapefiles de geoftp.ibge.gov.br emscripts/seed-ibge/data/(diretório ignorado pelo Git).scripts/seed-ibge/importar.tsvalida a geometria, converte para GeoJSON e insere emcore.uc_polygonscomfonte = 'ibge': municípios (nível 4), bairros do Censo 2022 (nível 2), UFs (nível 5), regiões (nível 6) e o polígono do Brasil (nível 7). Ao final, imprime o resumo por nível e por fonte.
O importador é idempotente pela chave fonte somada ao código da fonte nos metadados (codigo_municipio, codigo_uf, codigo_regiao e codigo_bairro). Rodar o mesmo seed duas vezes atualiza a geometria e o nome dos registros existentes, sem duplicar. Novo censo ou mudança de malha gera nova execução do seed, não uma migration. A validação de geometria (SRID 4326, anel fechado, auto-intersecção em anéis de até 2.000 vértices e área mínima de 100 m²) fica em scripts/utils/geo.ts.
Referências
Seção intitulada “Referências”- Ficha técnica da colônia: Apêndice B - Colônias.md, seção “D-2 — Georreferenciamento”
- Schemas de eventos: N-0b - Registry.md
- Barramento de eventos: N-0a - Event Bus.md
- Observabilidade: N-0c - Observabilidade.md
- Colônia a montante: D-1b - Normalização.md
- Colônias a jusante: D-3 - Categorização.md, D-4 - Priorização e Ranking.md, D-7 - Transparência.md
- Colônia análoga no fluxo de lugares: L-2 - Georreferenciamento e Tipificação.md
- Stack de referência e arquitetura do MVP: Apêndice B - Colônias.md, seção “Arquitetura do MVP — Monolito Modular”
- Mapa de dependências de eventos: Apêndice B - Colônias.md, seção “Mapa de Dependências de Eventos entre Colônias”
- Princípios do Formigueiro: Apêndice B - Colônias.md, seção “Princípios herdados do Formigueiro”
Documento de especificação técnica de implementação.