# Python SDK

## Instalacao

Requer Python >= 3.10.

```bash
pip install prismadata
```

Extras opcionais:

```bash
pip install prismadata[pandas]     # enriquecimento de DataFrames
pip install prismadata[sklearn]    # transformer scikit-learn
pip install prismadata[cache]      # cache local em disco
pip install prismadata[progress]   # barra de progresso (tqdm)
pip install prismadata[all]        # todos os extras
```

## Quick Start

```python
from prismadata import Client

client = Client(api_key="ak_xxxxxxxxxxxxxxxx")

# Consultar favela mais proxima
result = client.slum(lat=-23.55, lng=-46.63)
print(result)

# Geocodificar um endereco
geo = client.geocode(full_address="Av Paulista 1000, Sao Paulo")
print(geo)
```

## Autenticacao

O SDK aceita 3 formas de credencial (em ordem de prioridade):

**1. API Key direta:**

```python
client = Client(api_key="ak_xxxxxxxxxxxxxxxx")
```

**2. Usuario e senha:**

```python
client = Client(username="user@email.com", password="senha")
```

**3. Variaveis de ambiente:**

```bash
export PRISMADATA_APIKEY=ak_xxxxxxxxxxxxxxxx
# ou
export PRISMADATA_USERNAME=user@email.com
export PRISMADATA_PASSWORD=senha
```

```python
client = Client()  # detecta automaticamente
```

A renovacao do token JWT e automatica — o SDK cuida internamente.

## Uso Assincrono

```python
from prismadata import AsyncClient

async with await AsyncClient.create(api_key="ak_xxxxxxxxxxxxxxxx") as client:
    result = await client.slum(lat=-23.55, lng=-46.63)
    print(result)

    # Enriquecimento de DataFrames tambem funciona
    enriched = await client.enrich(df, services=["slum", "income_static"])
```

O `AsyncClient` usa factory pattern — note o `await` antes de `AsyncClient.create()`.
Todos os metodos do `Client` possuem equivalente assincrono no `AsyncClient`.

## Operacoes em Lote

Para consultas em volume, use os metodos `*_batch`:

```python
points = {
    "p1": [-23.55, -46.63],
    "p2": [-22.90, -43.17],
    "p3": [-19.92, -43.94],
}

# Batch de favelas
result = client.slum_batch(points)

# Batch de presídios
result = client.prison_batch(points)

# Batch de renda estatica
result = client.personal_income_static_batch(points)

# Batch de renda PDF (distribuicao de probabilidade)
result = client.personal_income_pdf_batch(points)

# Batch de renda PDF com parametros demograficos
result = client.personal_income_pdf_batch_detailed({
    "p1": {"lat": -23.55, "lng": -46.63, "gender": "M", "age": 50},
    "p2": {"lat": -22.90, "lng": -43.17, "gender": "F", "age": 30, "is_responsible": True},
})

# Batch com multiplos servicos (agregador)
result = client.aggregate_batch(
    points,
    services=["slum", "prison", "infosc"],
)
```

O SDK divide automaticamente lotes grandes em chunks e reenvia com retry em caso de erro.

### Geocodificacao em Lote

```python
# Geocodificar enderecos em lote (max 512 por chunk)
addresses = {
    "p1": {"endereco_completo": "Av Paulista 1000, Sao Paulo, SP"},
    "p2": {"cep": "30130-165", "numero": "100", "logradouro": "Rua da Bahia"},
}
result = client.geocode_batch(addresses)

# Geocodificacao reversa em lote (max 1024 por chunk)
points = {"p1": [-23.55, -46.63], "p2": [-22.90, -43.17]}
result = client.reverse_geocode_batch(points)

# Geocodificacao + agregacao em lote
result = client.geocode_aggregate_batch(
    addresses,
    services=["slum", "infosc"],
)
# Cada resultado contem campos do geocoder + dos servicos solicitados
```

## CNPJ

Consulta de dados cadastrais de pessoas juridicas (Receita Federal + geo).
O SDK valida o CNPJ localmente (mod-11 alfanumerico, aceita mascara) antes
de fazer a request.

```python
# Lookup individual - shape flat (49 chaves escalares)
r = client.cnpj("82.554.437/0001-31")
print(r["prismadata__pessoa_juridica__razao_social"])

# Lookup raw - shape F1 com socios/CNAEs como listas
r_raw = client.cnpj_raw("82554437000131")
for socio in r_raw["prismadata__pessoa_juridica__socios"]:
    print(socio["prismadata__pessoa_juridica__nome_ou_razao_social"])

# Batch flat (max 1024/chamada) - retorna dict indexado por CNPJ
batch = client.cnpj_batch(["82554437000131", "60701190076771"])
if batch["82554437000131"] is None:
    print("CNPJ nao encontrado")

# Batch raw (max 256/chamada - payload maior)
batch_raw = client.cnpj_raw_batch(["82554437000131"])

# Metadata do dataset
meta = client.cnpj_metadata()
print(meta["prismadata__pessoa_juridica__total_cnpjs_carregados"])
```

### CNPJ enriquecido com geo

O agregador anexa dados dos pilares geo (favela, presidio, fronteira, renda,
aglomeracao comercial, infosc, tipologia) numa unica chamada. Quando o CNPJ
nao tem lat/lng, as flags geo sao **silenciosamente puladas** — so vem o
bloco `prismadata__pessoa_juridica__*` com `qualidade_geocodificacao_classificacao="NAO_GEOCODIFICADO"`.

```python
r = client.cnpj_enriched(
    "82554437000131",
    slum=True, border=True, tipologia=True,
)
if "prismadata__slum__nome" in r:
    print("Favela mais proxima:", r["prismadata__slum__nome"])
else:
    print("Sem geo. Qualidade:",
          r.get("prismadata__pessoa_juridica__qualidade_geocodificacao_classificacao"))

# Batch enriquecido (max 256/chamada)
batch = client.cnpj_batch_enriched(
    ["82554437000131", "60701190076771"],
    slum=True, tipologia=True,
)
```

## Entorno (v1.15)

Estudo agregado do microterritório ao redor de um ponto — retorna renda,
concorrência, habitação, favelas e ACI numa única chamada. Ideal para
cientistas de dados: **1 dossiê = 1 linha de DataFrame**.

O `client.entorno()` retorna dict (padrão do SDK); o batch e o
`enrich(services=["entorno"])` retornam DataFrame (precisam do extra
`pandas`):

```bash
pip install 'prismadata[pandas]'
```

- `incluir_geometria=True` no `entorno()` adiciona uma chave escalar
  `geometria_estudo_wkt` no dict (contrato `entorno_srv` v1.2+).
- `detalhe_concorrencia="cruzado"` e `descricoes=True` continuam sendo
  ignorados no flat (emitem `UserWarning`) — use `entorno_raw()` para
  esses.
- Timeouts padrão: **35s** para chamadas unitárias, **60s** para batch
  (o pior caso realista do servidor é 27s; extremos até 39s).

Regras do request (validadas no cliente antes de sair):
- Exatamente um entre `isocrona`, `buffer` ou `wkt`.
- Se não for `wkt`: âncora via `endereco` **XOR** `latitude`+`longitude`.
- `isocrona` aceita `minutos` (1-30) **XOR** `metros` (100-50000).
- `buffer.raio_m` entre 100 e 20000.
- **v1.14**: campos desconhecidos em `isocrona`/`buffer`/`flags`/
  `filtros_cnpj`/`ancora` são rejeitados client-side com `ValueError`
  (o servidor também rejeita com 422). Typos comuns viram sugestão:
  `{"modo": "foot"}` → `did you mean 'profile'?`.

```python
# 1 dossiê flat como dict (~68 chaves escalares)
r = client.entorno(
    endereco="Rua Washington 660, Belo Horizonte",
    isocrona={"profile": "foot", "metros": 500, "sentido": "SAINDO"},
    flags={"favelas": True, "aci": True},
    filtros_cnpj={"cnae_prefix": ["4771"]},
)
print(r["concorrencia_total"], r["habitacao_total_domicilios_residenciais"])
# Para virar DataFrame de 1 linha: pd.DataFrame([r])

# Dossiê aninhado — pra explorar as listas (concorrência por CNAE, ACIs, etc)
dossie = client.entorno_raw(
    endereco="Rua Washington 660, Belo Horizonte",
    buffer={"raio_m": 500},
    detalhe_concorrencia="cruzado",
    descricoes=True,
)
print(list(dossie.keys()))  # habitacao, renda_pessoal, concorrencia, ...

# Batch — até 100 estudos por chamada. Falhas viram linha com sucesso=False
df_batch = client.entorno_batch({
    "sion":     {"endereco": "Rua Washington 660, BH", "buffer": {"raio_m": 500}},
    "paulista": {"endereco": "Av Paulista 1578, SP",   "buffer": {"raio_m": 500}},
})
print(df_batch[df_batch.sucesso][["id_cliente", "concorrencia_total"]])

# Enrich — pagina automático (chunks de 100) e faz merge por `id`
import pandas as pd
df_enderecos = pd.DataFrame({
    "id": ["sion", "aglomerado", "paulista"],
    "endereco": ["Rua Washington 660, BH", None, "Av Paulista 1578, SP"],
    "latitude": [None, -19.933, None],
    "longitude": [None, -43.920, None],
    "buffer_raio_m": [500, 400, 500],
})
enriquecido = client.enrich(df_enderecos, services=["entorno"])
```

Colunas aceitas pelo `enrich(services=["entorno"])`:
`id` (opcional, usa índice do df se ausente), `endereco` OU
`latitude`+`longitude`, `wkt`, `buffer_raio_m` (default 500),
`isocrona_profile`/`isocrona_metros`/`isocrona_minutos`/`isocrona_sentido`.
Erros 422 do servidor viram `EntornoValidationError` (herda de
`ValidationError` — código antigo que captura `ValidationError` continua
funcionando).

### Origem dos dados (`origem_dados`)

Toda resposta v1.14 carrega uma chave `origem_dados` com valor `"prod"`
ou `"sandbox"` — auto-declaração do servidor, para dev não confundir
tela com dado sintético. Aparece em qualquer forma da resposta:

```python
r = client.entorno(latitude=-19.9, longitude=-43.9, buffer={"raio_m": 500})
r["origem_dados"]                        # "prod" ou "sandbox"
client.entorno_provenance(r)              # idem, via helper
client.entorno_provenance(dossie_raw)     # também funciona no dossiê aninhado
df_batch.groupby("origem_dados").size()   # separa prod × sandbox no batch
```

### Errata do bloco `favelas` (v1.14 Item 1.1)

O bloco tem dois campos com **semânticas diferentes** — cuidado ao
comparar:

| Campo (flat) | Semântica | Uso |
|---|---|---|
| `favelas_domicilios_particulares` | DOMPP das favelas **inteiras** que intersectam o estudo | Contexto do bairro popular |
| `favelas_domicilios_residenciais` | Domicílios CNEFE dentro de `estudo ∩ favelas` (**recortado**) | Densidade real dentro do estudo |

Casos reais (bateria v5 prod):
- Buffer 500m no Rio (2% de favela): `5153` vs `322` — razão 16× (favela
  é muito maior que o buffer).
- Isócrona car 10min em BH com âncora dentro da favela: `56998` vs
  `54385` — razão 1.05× (favelas contidas no estudo).

### Paridade de shape (v1.14 Item 2.3)

As três colunas de benchmark aparecem **sempre** que o bloco
`concorrencia` existe: `concorrencia_benchmark_cod_municipio`,
`concorrencia_benchmark_municipio_por_1000_domicilios`,
`concorrencia_indice_vs_municipio`. Em prod podem vir `None` (quando o
`cnpj_srv` upstream não devolveu agregado). Não use `df.dropna(axis=1)`
para descobrir colunas presentes — mantenha lista canônica.

Em sandbox, `concorrencia_benchmark_cod_municipio` deriva do município
da âncora (SP → 3550308, RJ → 3304557, BH → 3106200 etc.), não é mais
hardcoded.

### Bloco `huff` (v1.15) — partição de demanda

Modelo probabilístico que particiona os domicílios do estudo entre a
âncora e os rivais visíveis, devolvendo o triple **cativos / disputados
/ perdidos** + share esperado. Opt-in via `huff=True` (defaults do
backend) ou `huff=dict` com overrides.

Requer:

- `ancora={"cnpj": "..."}` — o modelo precisa de um PDV concreto para
  identificar qual alternativa é a "própria".
- `filtros_cnpj` com `cnae_prefix` ou `segmento` — define o universo
  dos rivais.

Ambos são validados client-side antes do request.

Overrides aceitos pelo dict:

| Chave | Faixa | Default no backend |
|---|---|---|
| `origens` | `"setores"` | `"enderecos"` | auto (setores para buffers grandes) |
| `lambda` | `"conveniencia"` | `"padrao"` | `"destino"` | `"padrao"` (λ=2.0) |
| `epsilon_disputa` | 0.01 - 0.30 | 0.10 |
| `max_alternativas` | 3 - 20 | 10 |
| `metodo_distancia` | `"geodesica"` (rota reservado v2) | `"geodesica"` |

Exemplo canônico (Wanessa, BH):

```python
resp = client.entorno(
    ancora={"cnpj": "14169897001915"},
    buffer={"raio_m": 500},
    filtros_cnpj={"cnae_prefix": ["4771"]},
    huff=True,
)

print(f"cativos:        {resp['huff_dom_cativos']:>5} ({resp['huff_frac_cativos']:>5.1%})")
print(f"disputados:     {resp['huff_dom_disputados']:>5} ({resp['huff_frac_disputados']:>5.1%})")
print(f"perdidos:       {resp['huff_dom_perdidos']:>5} ({resp['huff_frac_perdidos']:>5.1%})")
print(f"share esperado: {resp['huff_share_esperado']:.1%}")
# Ver todas as 21 colunas: [k for k in resp if k.startswith('huff_')]
```

Invariante (útil para dashboards):

```
huff_dom_cativos + huff_dom_disputados + huff_dom_perdidos
  == huff_n_unidades_origens
  <= habitacao_total_domicilios_residenciais
```

A diferença entre `huff_n_unidades_origens` e
`habitacao_total_domicilios_residenciais` são domicílios coletivos
(asilos, pensões, presídios) — o motor Huff filtra só
`DOMICILIO_PARTICULAR` por LGPD + coerência com `renda_srv`. Quando
> 0, o backend anexa aviso automático em `execucao.avisos` (visível via
`entorno_raw()`).

Sensibilidade λ (interpretação amigável):

- `huff_share_min` — share com λ=2.5 (decaimento agressivo: freguês
  mais preso ao ponto próximo).
- `huff_share_esperado` — share com λ=2.0 (padrão).
- `huff_share_max` — share com λ=1.5 (decaimento suave: freguês
  disposto a percorrer distância).

Sempre `share_min ≤ share_esperado ≤ share_max`.

**Duas colunas de distância no `particao_por_alternativa`** (só no /raw,
não vira flat):

- `distancia_m_min_origem` — mín distância entre qualquer endereço
  residencial no polígono e a filial mais próxima da rede. Métrica
  interna do modelo (pode ser ~0 quando um apartamento residencial
  está no mesmo prédio comercial que uma farmácia).
- `distancia_m_ate_ancora` — haversine entre o ponto âncora e a filial
  mais próxima da rede. Interpretação comercial direta. Para a própria
  âncora, é 0.

Contrato oficial em `docs.prismadata.io/entorno` (v1.15).

## IP Info (v0.16) — produto identity

Inteligência de IP: dados de geolocalização, ASN, VPN/proxy detection,
cross-check contra listas de sanções, e emissão de relatórios auditáveis
assinados via JWS.

```python
# Consulta simples por IP
r = client.ipinfo(input_type="ip", ip="8.8.8.8", enrich=True)

# Consulta por coordenada
r = client.ipinfo(input_type="coordinate", lat=-23.55, lng=-46.63)

# Batch — até 1024 itens por chamada
r = client.ipinfo_batch(
    {"a": {"ip": "8.8.8.8"}, "b": {"ip": "1.1.1.1"}},
    input_type="ip",
    check_sanctions=True,
)
```

Toda resposta é entregue assinada (JWS). Você pode verificar no servidor
ou localmente:

```python
# Verificação server-side
r = client.ipinfo_verify(signed_response="eyJhbGc...ABC")

# Chaves para verificação local
keys = client.ipinfo_signing_keys()
```

Relatórios auditáveis das consultas passadas:

```python
# Lista paginada com filtros
r = client.ipinfo_reports(
    date_from="2026-01-01",
    is_vpn=True,
    country_code="BR",
    page=1, page_size=50,
)

# Detalhe de 1 consulta
r = client.ipinfo_report("query-id-uuid")

# Download completo (sem paginação)
r = client.ipinfo_reports_download(is_sanctioned=True)
```

## Buffer geodésico (v0.15)

Quando você só precisa de um círculo aproximado ao redor de um ponto —
sem malha viária, sem transito, sem tempo — use `client.buffer()`. É
muito mais barato que `isochrone()`.

```python
r = client.buffer(lat=-23.55, lng=-46.63, radius_m=500)
r["prismadata__routing_isochrone__area_m2"]       # ~78539.8 m² (πr²)
r["prismadata__routing_isochrone__geometria"]     # WKT Polygon
r["prismadata__routing_isochrone__num_vertices"]  # 64 (default)

# num_vertices controla a resolucao do poligono (8-256)
r = client.buffer(lat=-23.55, lng=-46.63, radius_m=1000, num_vertices=128)

# Batch — mesma envelope de route_batch/isochrone_batch
r = client.buffer_batch([
    {"lat": -23.55, "lng": -46.63, "raio_m": 500},
    {"lat": -22.90, "lng": -43.17, "raio_m": 800, "num_vertices": 32},
])
```

Raio: 10 m a 100 km (cap do servidor).

Observação: os campos compartilham o prefixo
`prismadata__routing_isochrone__` com `isochrone()` — mesmo namespace
no servidor.

## Fronteiras

O método `client.border(lat, lng)` devolve 21 campos escalares que descrevem
a posição do ponto em relação à malha fronteiriça brasileira. Os campos
cobrem 4 dimensões distintas — não confundir as métricas:

**Distâncias ao país mais próximo (2 medições diferentes)**

- `pais_mais_proximo_pt` + `distancia_geodesica_pais_mais_proximo_pt_km` —
  distância do **ponto consultado** até a fronteira do país estrangeiro.
- `pais_mais_proximo` (mesmo país) + `distancia_geodesica_pais_mais_proximo_sede_municipio_km` —
  distância da **sede do município** que contém o ponto até o mesmo país
  estrangeiro. Útil quando o ponto está em zona rural distante da sede.
- `segundo_pais_mais_proximo_pt` + `distancia_geodesica_segundo_pais_km` —
  segundo colocado (para pontos com dois países equidistantes).
- `arco_fronteira` — arco geográfico (norte / centro-oeste / sul).

**Faixa de fronteira (Lei 6.634/79, 150 km da linha)**

- `ponto_contido_na_faixa_de_fronteira: bool` — o ponto consultado está
  dentro dos 150 km.
- `distancia_geodesica_ponto_borda_faixa_de_fronteira_km: float` — distância
  do ponto até o limite interno da faixa.
- `nome_municipio_de_fronteira` / `area_total_municipio_km2` /
  `area_municipio_integrada_faixa_de_fronteira_km2` /
  `frac_area_municipio_faixa_de_fronteira` /
  `sede_municipio_dentro_da_faixa_de_fronteira: bool` — caracterização do
  município.

**Cidades-gêmeas** (pares binacionais oficiais reconhecidos pela Portaria
Interministerial 213/2016)

- `municipio_possui_cidade_gemea: bool`
- `nome_cidade_gemea_estrangeira` — ex: `"Ciudad del Este"`
- `pais_cidade_gemea`

**Tríplice fronteira**

- `ponto_em_area_triplice_fronteira: bool` — dentro do raio configurável
  (default 50 km — ver `RAIO_AREA_TRIPLICE_FRONTEIRA_KM` no
  `/border/metadata`).
- `distancia_triplice_fronteira_km: float | None`
- `nome_triplice_fronteira: str | None` — ex: `"Tríplice Fronteira do
  Iguaçu"`.
- `paises_triplice_fronteira: str | None` — ex:
  `"ARGENTINA/BRASIL/PARAGUAI"`.

**Score de exposição a fronteira** (adicionado no upstream — v0.17 do
SDK): 17 campos derivados dos anteriores compõem um índice
determinístico 0-100 que serve como feature única para modelos de
risco / KYC / PLD-FT.

- `score_exposicao_fronteira: int` — 0 a 100.
- `classificacao_exposicao_fronteira: str` — banda categórica:
  `RESIDUAL` (0-19), `BAIXA` (20-44), `MODERADA` (45-69) ou `ELEVADA`
  (70-100).
- `versao_metodologia_score: str` — semver da metodologia usada. Versões
  anteriores permanecem documentadas em `/border/metadata`.

Quatro componentes ponderados (todos `int | None`; nulos fora de
município de fronteira):

| Campo | Peso default | O que mede |
|---|---:|---|
| `score_componente_proximidade_internacional` (D1) | 0.35 | Proximidade à linha internacional, escala log ancorada em 100/25/0 |
| `score_componente_triplice_fronteira` (D2) | 0.30 | Proximidade a ponto de tríplice fronteira (100 no Iguaçu) |
| `score_componente_cidade_gemea` (D3) | 0.20 | 100 se o município tem cidade-gêmea, senão 0 |
| `score_componente_intensidade_municipal` (D4) | 0.15 | Fração da faixa no município × sede na faixa |

Sete fatores booleanos (bandeiras determinísticas — úteis pra filtros e
explicabilidade):

- `fator_ponto_na_faixa`
- `fator_sede_na_faixa`
- `fator_municipio_integralmente_na_faixa`
- `fator_cidade_gemea`
- `fator_area_influencia_triplice`
- `fator_triplice_iguacu`
- `fator_ponto_fora_do_territorio_nacional` (informacional, não entra no
  cálculo)

Metadados de acionamento:

- `fatores_acionados: str` — nomes dos fatores em ordem canônica,
  separados por `|` (string vazia se nenhum). Útil pra `groupby` /
  filtro em DataFrame.
- `qtd_fatores_acionados: int` — contagem de fatores True.
- `piso_aplicado: str | None` — quando um piso regulatório elevou o
  índice acima do calculado, aparece o rótulo (ex.:
  `TRIPLICE_FRONTEIRA_GAFI`). Nulo quando não houve piso.

```python
r = client.border(lat=-25.5478, lng=-54.5882)   # Foz do Iguaçu
print(r["prismadata__border__score_exposicao_fronteira"])         # 100
print(r["prismadata__border__classificacao_exposicao_fronteira"]) # "ELEVADA"
print(r["prismadata__border__fatores_acionados"])                 # "PONTO_NA_FAIXA|CIDADE_GEMEA|..."
print(r["prismadata__border__piso_aplicado"])                     # "TRIPLICE_FRONTEIRA_GAFI"
```

Os pesos, âncoras e limiares da metodologia ficam em
`client.border_metadata()["prismadata__border__score"]` — inclui
`VERSAO_METODOLOGIA`, `PESOS`, `D0_KM`, `ANCORAS_D1`,
`LIMIAR_TRIPLICE_KM` etc. Consulte antes de calibrar um modelo em cima
do índice — versões novas podem ajustar pesos.

Parâmetros de cálculo (raios usados internamente) e a lista canônica de
pontos de tríplice fronteira ficam em `client.border_metadata()`:

```python
meta = client.border_metadata()
params = meta["prismadata__border__parametros"]
print(params["RAIO_AREA_TRIPLICE_FRONTEIRA_KM"])       # 50.0 (default)
print(params["ALCANCE_MAXIMO_TRIPLICE_FRONTEIRA_KM"])  # 500.0
for p in meta["prismadata__border__pontos_triplice_fronteira"]:
    print(p["NOME"], p.get("PAISES"))
```

Exemplo (Foz do Iguaçu — dentro da tríplice fronteira do Iguaçu):

```python
r = client.border(lat=-25.5478, lng=-54.5882)
if r["prismadata__border__ponto_em_area_triplice_fronteira"]:
    print(
        r["prismadata__border__nome_triplice_fronteira"],
        r["prismadata__border__paises_triplice_fronteira"],
        f"{r['prismadata__border__distancia_triplice_fronteira_km']:.1f} km",
    )
# -> Tríplice Fronteira do Iguaçu ARGENTINA/BRASIL/PARAGUAI 7.8 km
```

## Tipologia (CNEFE)

Tipologia retorna a finalidade (residencial, comercial, misto), especie cadastral
e tipo de moradia do endereco CNEFE mais proximo. Base: Censo CNEFE-IBGE 2022,
cobertura aproximada de 65% do territorio brasileiro.

```python
# Por coordenada
result = client.tipologia(lat=-23.55, lng=-46.63)
# result["prismadata__tipologia__finalidade"] -> "RESIDENCIAL" / "NAO_RESIDENCIAL" / ...
# result["prismadata__tipologia__cobertura"] -> bool

# cobertura=False nao e erro - decide o que fazer no cliente
if not result["prismadata__tipologia__cobertura"]:
    motivo = result.get("prismadata__tipologia__motivo_sem_cobertura")
    # FORA_DE_AREA_BRASIL | CNEFE_NAO_CADASTRADO | DISTANCIA_EXCESSIVA

# Por endereco textual ou estruturado
result = client.tipologia_by_address(full_address="Av Paulista 1000, Sao Paulo, SP")
# ou com campos individuais (tem prioridade)
result = client.tipologia_by_address(
    zipcode="01310-100", number=1000, city="Sao Paulo", state="SP",
)
# result["prismadata__tipologia__match_method"] indica match exato ou aproximado

# Metadata do dataset (versao, cobertura)
meta = client.tipologia_metadata()
print(meta["prismadata__tipologia__versao_dados"])

# Batch por coordenada (servidor caps a 1024/chunk - SDK chunka automaticamente)
result = client.tipologia_batch({
    "p1": [-23.55, -46.63],
    "p2": [-22.90, -43.17],
})

# Batch por endereco (servidor caps a 512/chunk)
result = client.tipologia_by_address_batch({
    "a1": {"endereco_completo": "Av Paulista 1000, Sao Paulo, SP"},
    "a2": {"cep": "30130-165", "numero": "100"},
})
```

Tipologia tambem esta disponivel via aggregator (consulte multiplos produtos
numa unica chamada):

```python
result = client.aggregate(
    -23.55, -46.63,
    services=["tipologia", "slum", "income_static"],
)
# ou em batch
result = client.aggregate_batch(
    {"p1": [-23.55, -46.63], "p2": [-22.90, -43.17]},
    services=["tipologia", "income_static"],
)
```

**Aviso sobre confianca**: em enderecos multi-unidade (shoppings, condominios,
predios comerciais), o servidor retorna `indicador_estabelecimento=MULTIPLO_*`
e `confianca` pode ser `MEDIA` ou `BAIXA`. Esses campos indicam que o
resultado e uma amostra, nao a unidade especifica. Veja documentacao completa
em https://docs.prismadata.io/tipologia.

## Aglomeracoes Comerciais

Aglomeracoes comerciais sao poligonais continuas de tecido comercial denso (~11k
regioes no Brasil), com atributos como qtd de PDVs, score, porte, vocacao,
faixa de renda e trechos de rua ranqueados.

```python
# Aglomeracao + trecho de rua mais proximo de um ponto
result = client.commercial_cluster(lat=-23.5613, lng=-46.6558)

# Top 3 aglomeracoes proximas (resposta indexada por 1_, 2_, 3_)
result = client.commercial_cluster(
    lat=-23.5613, lng=-46.6558, top_n=3, radius_km=10.0,
)

# Filtrar por vocacao (aceita string ou lista)
result = client.commercial_cluster(
    lat=-23.5613, lng=-46.6558,
    vocation=["Moda", "Alimentacao"],
    min_size="Grande",
)

# Enriquecimento de carteira de PDVs (ate 1024 pontos por request)
carteira = {
    "pdv_1": [-23.5613, -46.6558],
    "pdv_2": [-22.9068, -43.1729],
}
result = client.commercial_cluster_batch(carteira, top_n=1)
# result = {"pdv_1": {...campos flat...}, "pdv_2": {...}}

# Drill-down: detalhes de uma aglomeracao + todos os trechos
detail = client.commercial_cluster_detail("5ebd0eb4f6b4")

# Geometria (opcional, premium): GeoJSON dict ou WKT string
# Requer role `admin` ou `viewer:commercial_cluster` no JWT - fale com vendas.
r = client.commercial_cluster(-23.5613, -46.6558, include_geometry=True)
geom = r["prismadata__commercial_cluster__geometria"]
# geom e dict: {"type": "Polygon", "coordinates": [[[...]]]}

r_wkt = client.commercial_cluster(-23.5613, -46.6558, include_geometry=True, wkt=True)
# r_wkt[...geometria] e str: "POLYGON((...))"

# Sem a role: levanta GeometryNotAuthorizedError (HTTP 403).
# Batch nao suporta geometria - usar single point ou detail.
# Tentar incluir_geometria em batch levanta GeometryBatchNotSupportedError.

# Granularidade fina: trecho de rua mais proximo (raio em metros)
street = client.commercial_cluster_street(-23.5613, -46.6558, radius_m=200.0)

# Busca por filtros (sem ponto geografico) - lista paginada
results = client.commercial_cluster_search(
    filters={
        "state": ["SP", "RJ"],
        "vocation": ["Moda"],
        "size": ["Grande"],
        "income_range": ["Premium", "Luxo"],
        "min_pdv_count": 500,
        "bbox": {"min_lat": -24, "min_lng": -47, "max_lat": -23, "max_lng": -46},
    },
    limit=50,
)

# Ranking top-N por criterio e escopo
top_sp = client.commercial_cluster_ranking(
    criterion="score", scope="state", state="SP", top_n=20,
)
```

Para combinar `commercial_cluster` com outros produtos numa unica chamada,
use o aggregator (caminho preferido para enriquecimento de carteira):

```python
# Por ponto
result = client.aggregate(
    -23.5613, -46.6558,
    services=["commercial_cluster", "slum", "prison"],
    commercial_cluster_top_n=3,
    commercial_cluster_raio_km=10.0,
)

# Por endereco textual (geocoda + enriquece)
result = client.geocode_aggregate(
    full_address="Av Paulista 1000, Sao Paulo, SP",
    services=["commercial_cluster", "slum"],
)

# Batch
result = client.aggregate_batch(
    carteira,
    services=["commercial_cluster", "border"],
    commercial_cluster_top_n=1,
)
```

## Enriquecimento de DataFrames

Com o extra `pandas`, enriqueca DataFrames diretamente:

```python
import pandas as pd
from prismadata import Client

client = Client(api_key="ak_xxxxxxxxxxxxxxxx")

df = pd.DataFrame({
    "lat": [-23.55, -22.90, -19.92],
    "lng": [-46.63, -43.17, -43.94],
})

enriched = client.enrich(
    df,
    services=["slum", "prison", "income_static"],
)
print(enriched.columns.tolist())
```

## Pipeline Scikit-learn

Com o extra `sklearn`, use o transformer em pipelines de ML:

```python
from sklearn.pipeline import Pipeline
from prismadata import Client

client = Client(api_key="ak_xxxxxxxxxxxxxxxx")

transformer = client.get_transformer(
    services=["slum", "income_static"],
    lat_col="latitude",
    lng_col="longitude",
)

pipe = Pipeline([
    ("prismadata", transformer),
    ("model", your_model),
])
```

## Tratamento de Erros

```python
from prismadata import Client
from prismadata.exceptions import (
    AuthenticationError,
    BatchError,
    RateLimitError,
    PrismaDataError,
)

try:
    client = Client(api_key="ak_xxxxxxxxxxxxxxxx")
    result = client.slum(lat=-23.55, lng=-46.63)
except AuthenticationError:
    print("Credenciais invalidas")
except RateLimitError:
    print("Rate limit excedido, aguarde e tente novamente")
except PrismaDataError as e:
    print(f"Erro da API: {e}")
```

### Falhas Parciais em Batch

Quando um lote grande falha parcialmente, o SDK coleta os resultados dos chunks
que deram certo e levanta `BatchError` com os dados parciais:

```python
try:
    result = client.slum_batch(large_point_dict)
except BatchError as e:
    # Resultados dos chunks que deram certo
    for key, value in e.partial_results.items():
        process(key, value)

    # Chaves dos chunks que falharam (para retry manual)
    print(f"Falhou: {e.failed_keys}")
```

O SDK implementa retry automatico com backoff exponencial para erros transientes (429, 500, 502, 503, 504).
Alem disso, respeita os headers `Retry-After` e `X-RateLimit-Reset` do servidor.

---

- Índice geral: https://prismadata.io/llms.txt
- Documentação Redoc interativa: https://prismadata.io/redoc.html
