2

22,8% dos endereços do país não têm número: rodei a validação na base de entregas e 18,56% não passaram

O TI Inside publicou hoje o número do CNEFE, o cadastro de endereços do IBGE: dos 106,8 milhões de endereços levantados no Censo 2022, 24,4 milhões não têm número. São 22,8% do total. Outros 2,7 milhões ficam em rua sem nome e 438 mil são identificados apenas pelo quilômetro da rodovia.

Esse é um número de país. Ele não serve para decidir nada no seu sistema. O que serve é o mesmo número medido na sua tabela, e essa conta leva uns dez minutos.

O motivo de eu ter ido medir: a reclamação que chegou não falava de endereço. Falava de modelo. O agente que monta a rota do dia estava produzindo sequência ruim, entregador voltando no mesmo bairro duas vezes, e a proposta na mesa era trocar o modelo por um maior. Antes disso eu pedi acesso de leitura à tabela de endereços.

A validação mínima

A base é de uma operação B2C, Postgres 15, 214.015 linhas de endereço acumuladas em pouco mais de três anos. A checagem é o mais burro possível: CEP com 8 dígitos, logradouro preenchido, número que seja um número, e nada de quilômetro de rodovia no meio do logradouro.

WITH base AS (
  SELECT id,
         coalesce(logradouro, '')  AS logradouro,
         coalesce(numero, '')      AS numero,
         coalesce(cep, '')         AS cep
  FROM enderecos_entrega
),
classificado AS (
  SELECT id,
    CASE
      WHEN regexp_replace(cep, '\D', '', 'g') !~ '^[0-9]{8}$'
        THEN 'cep_invalido'
      WHEN btrim(logradouro) = ''
        THEN 'sem_logradouro'
      WHEN btrim(numero) = '' OR numero ~* '^(s/?n|sn|0+|-|\.)$'
        THEN 'sem_numero'
      WHEN logradouro ~* '(^|[^a-z])km[ .]?[0-9]'
        THEN 'quilometro_rodovia'
      WHEN length(btrim(logradouro)) < 6
        THEN 'logradouro_suspeito'
      ELSE 'ok'
    END AS classe
  FROM base
)
SELECT classe,
       count(*) AS qtd,
       round(100.0 * count(*) / sum(count(*)) OVER (), 2) AS pct
FROM classificado
GROUP BY classe
ORDER BY qtd DESC;

Saída:

       classe        |  qtd   |  pct
---------------------+--------+-------
 ok                  | 174293 | 81.44
 sem_numero          |  26118 | 12.20
 cep_invalido        |   5471 |  2.56
 logradouro_suspeito |   4022 |  1.88
 quilometro_rodovia  |   2884 |  1.35
 sem_logradouro      |   1227 |  0.57
(6 rows)

39.722 linhas, 18,56% da base, não passam numa validação que qualquer um escreve antes do café. Perto de um quinto. Em nenhuma dessas o agente tinha chance: ele recebeu uma string que não resolve para um ponto no mapa e devolveu o melhor palpite dele, que é exatamente o comportamento esperado de um modelo de linguagem.

Os valores do campo numero nessas linhas são os de sempre: S/N, SN, 0, 00, -, string vazia. Ninguém digitou errado por descuido. O formulário pediu um número para um endereço que não tem número, e alguém precisava clicar em salvar para a venda fechar.

Endereço sem número não está inválido, está sem âncora

Essa foi a parte que mudou a conversa. Jogar 26 mil endereços numa fila de correção manual é inviável e também desnecessário, porque boa parte deles já foi entregue antes, e o aplicativo do entregador gravou a coordenada da conclusão.

SELECT count(*) FILTER (WHERE h.lat IS NOT NULL) AS com_ancora,
       count(*)                                   AS total
FROM classificado c
JOIN enderecos_entrega e ON e.id = c.id
LEFT JOIN LATERAL (
  SELECT lat, lng
  FROM entregas_concluidas
  WHERE endereco_id = e.id AND lat IS NOT NULL
  ORDER BY concluida_em DESC
  LIMIT 1
) h ON true
WHERE c.classe = 'sem_numero';
 com_ancora | total
------------+--------
      16011 |  26118
(1 row)

Uns 61% já tinham um ponto confiável no histórico, melhor que qualquer geocodificação por CEP, porque é o lugar onde a mercadoria efetivamente chegou. Sobram 10.107 endereços, algo perto de 4,7% da base, que precisam de ponto de referência escrito por uma pessoa ou de uma ligação antes de sair com o veículo.

Para o centroide do CEP eu não tenho boa notícia. Em CEP de faixa única de cidade pequena, o centroide cai no meio do município e o roteirizador monta a rota em cima de um ponto que não existe. Pior que recusar: ele devolve uma sequência plausível e errada.

O contrato de entrada vai antes do agente

A mudança que entrou não foi de modelo. Foi uma função pura na borda, que decide quem entra na rota automática.

type Endereco = {
  logradouro: string
  numero: string
  cep: string
  referencia?: string
  lat?: number
  lng?: number
}

const SEM_NUMERO = /^(s\/?n|sn|0+|-|\.)?$/i

type Veredito =
  | { roteavel: true; ancora: 'numero' | 'coordenada' | 'referencia' }
  | { roteavel: false; motivo: string }

export function avaliar(e: Endereco): Veredito {
  if (!/^\d{8}$/.test(e.cep.replace(/\D/g, '')))
    return { roteavel: false, motivo: 'cep_invalido' }

  if (btrim(e.logradouro).length < 6)
    return { roteavel: false, motivo: 'logradouro_suspeito' }

  if (SEM_NUMERO.test(e.numero.trim())) {
    if (e.lat != null && e.lng != null)
      return { roteavel: true, ancora: 'coordenada' }
    if (btrim(e.referencia ?? '').length >= 12)
      return { roteavel: true, ancora: 'referencia' }
    return { roteavel: false, motivo: 'sem_numero_sem_ancora' }
  }

  return { roteavel: true, ancora: 'numero' }
}

const btrim = (s: string) => s.replace(/^\s+|\s+$/g, '')

O que não passa não vai para o agente. Vai para uma fila com nome, com motivo e com contador, e o contador é a parte que faz diferença na reunião seguinte: sem_numero_sem_ancora virou uma linha de painel que a operação olha todo dia, e caiu de 10.107 para algo perto de 7.400 em seis semanas, só por captura de referência no momento da primeira entrega.

Se você não sabe fazer massa de cimento, tanto faz a enxada ou a betoneira. Vai sair ruim das duas. Trocar o modelo com 18% da entrada quebrada muda o formato do erro e não o tamanho dele, e custa mais por token para continuar errando.

Uma coisa que eu não sei: não testei isso em base de operação rural ou de rodovia, onde o quilometro_rodovia provavelmente deixa de ser 1,35% e passa a ser a regra. Ali o numero talvez devesse ser um campo de outro tipo desde o começo.

Como você resolve endereço sem número hoje: manda para fila humana, aceita o centroide do CEP, ou usa a coordenada da última entrega concluída? E se usa coordenada, qual a janela que você considera válida antes de desconfiar que o cliente mudou de casa?

Fonte do dado nacional: reportagem do TI Inside de hoje sobre o CNEFE/IBGE. Sobre a conta de manutenção que esse tipo de atalho gera quando o agente entra em produção sem contrato de entrada, escrevi no blog da Revin: https://revin.com.br/pt/blog/divida-tecnica-de-software-agente-de-ia

Carregando publicação patrocinada...
1

A coordenada da última entrega é onde o pacote foi marcado como entregue. Isso pode ser a portaria ou o vizinho, não a porta do cliente. Uma janela de dias não resolve a mudança de casa: a entrega de ontem continua dentro da janela e o cliente já saiu.

O ponto antigo deixa de valer quando o texto muda. CEP ou logradouro diferente, a coordenada anterior é outro endereço. Se só o complemento mudou, o ponto do prédio ainda serve. A query do post pega a última linha com lat preenchida e não compara com o endereço escrito de agora. Essa comparação vem antes de tratar a coordenada como âncora.