Respan Dataset Explorer

Select one behavior. Every returned turn has one binary label: Present or Absent. Source: final dense boolean release.

5,167,182physical rows
86shards
0.00%qualified row coverage
0.00%qualified cell coverage
Random row JSON API

turns-00069.parquet:36811

d5e6a13425a288a2c5a8cfb7
turn 1/1gpt-4.1-mini-2025-04-14EnglishUnited Kingdom742 words
degenerate_repetitionAbsentFinal dense release
USER
     Goal
    Traduire toutes les valeurs textuelles présentes dans une liste d’objets d’une recette de cuisine de en (ISO 639) vers es (ISO 639), en respectant le contexte culinaire.

    Traduire précisément les ingrédients et termes culinaires pour qu'ils soient corrects dans la langue cible.
    Maintenir la structure JSON intacte, en ne modifiant que les valeurs.
    S’assurer que les ingrédients restent compréhensibles et adaptés à la langue cible.

    Return Format
    Tu dois renvoyer une liste d’objets sous la forme reçu.
    - Chaque valeur doit être correctement traduite en es.
    - Les ingrédients doivent être adaptés à la langue cible.
    - Aucune clé ne doit être modifiée.

    Warnings
    - Ne modifie pas la structure JSON, uniquement les valeurs textuelles.
    - Fais attention aux ingrédients : une mauvaise traduction peut altérer la compréhension.
    - Ne laisse aucune valeur non traduite.
    - Ne renvoie que le JSON, sans explication.

    Context Dump
    Langue source : en (ISO 639)
    Langue cible : es (ISO 639)
    Données à traduire :

    [
  {
    "recipeCode": "affb2342dbf246f3bd871f999256b2ac",
    "updated": "2025-04-21T20:22:11.138Z",
    "title": "Mushroom Soup",
    "subtitle": "A simple and flavorful mushroom soup.",
    "description": "This recipe highlights the rich flavor of mushrooms. A blend of porcini and cremini mushrooms is recommended, but common button mushrooms can be substituted, though the flavor might be less intense. Feel free to personalize the recipe by experimenting with different mushroom varieties, herbs, and wines.",
    "ingredients": [
      {
        "section": "For the recipe",
        "ingredients": [
          "low-sodium chicken broth",
          "fresh thyme leaves or dried thyme",
          "potato, cubed",
          "olive oil and unsalted butter as needed",
          "chopped onion",
          "garlic, minced",
          "sliced mushrooms",
          "lemon juice",
          "freshly ground black pepper to taste",
          "red or white wine",
          "milk",
          "fresh parsley or thyme for garnish"
        ]
      }
    ],
    "instructions": [
      "Combine the chicken broth, thyme, and cubed potato in a saucepan. Simmer until the potato is tender.",
      "Using a blender or food processor, purée the mixture until smooth. Return the purée to the saucepan.",
      "In a sauté pan, heat a small amount of olive oil and unsalted butter over medium heat. Add the chopped onion, minced garlic, and sliced mushrooms.",
      "Sauté for several minutes, until the onions become translucent and the mushrooms are tender.",
      "Season with lemon juice and freshly ground black pepper to taste.",
      "Incorporate the sautéed mushroom mixture into the potato purée.",
      "Deglaze the sauté pan with the wine, stirring with a wooden spoon. Add the deglazing liquid to the combined soup mixture.",
      "Gently warm the soup over low heat while gradually adding milk to achieve the desired consistency.",
      "Ladle the soup into bowls and garnish with fresh parsley or thyme sprigs."
    ],
    "notes_ingredients": "",
    "notes_instructions": ""
  }
]

    
ASSISTANT
[
  {
    "recipeCode": "affb2342dbf246f3bd871f999256b2ac",
    "updated": "2025-04-21T20:22:11.138Z",
    "title": "Sopa de champiñones",
    "subtitle": "Una sopa de champiñones simple y sabrosa.",
    "description": "Esta receta resalta el rico sabor de los champiñones. Se recomienda una mezcla de champiñones porcini y cremini, pero se pueden sustituir por champiñones blancos comunes, aunque el sabor podría ser menos intenso. Siéntase libre de personalizar la receta experimentando con diferentes variedades de champiñones, hierbas y vinos.",
    "ingredients": [
      {
        "section": "Para la receta",
        "ingredients": [
          "caldo de pollo bajo en sodio",
          "hojas de tomillo fresco o tomillo seco",
          "patata, en cubos",
          "aceite de oliva y mantequilla sin sal según sea necesario",
          "cebolla picada",
          "ajo picado",
          "champiñones en rodajas",
          "jugo de limón",
          "pimienta negra recién molida al gusto",
          "vino tinto o blanco",
          "leche",
          "perejil fresco o tomillo para decorar"
        ]
      }
    ],
    "instructions": [
      "Combine el caldo de pollo, el tomillo y la patata en cubos en una cacerola. Cocine a fuego lento hasta que la patata esté tierna.",
      "Usando una licuadora o procesador de alimentos, haga un puré con la mezcla hasta que esté suave. Devuelva el puré a la cacerola.",
      "En una sartén, caliente una pequeña cantidad de aceite de oliva y mantequilla sin sal a fuego medio. Añada la cebolla picada, el ajo picado y los champiñones en rodajas.",
      "Sofría durante varios minutos, hasta que las cebollas estén translúcidas y los champiñones tiernos.",
      "Sazone con jugo de limón y pimienta negra recién molida al gusto.",
      "Incorpore la mezcla de champiñones salteados al puré de patata.",
      "Desglase la sartén con el vino, removiendo con una cuchara de madera. Añada el líquido de desglasado a la mezcla combinada de la sopa.",
      "Caliente la sopa suavemente a fuego bajo mientras añade gradualmente leche para alcanzar la consistencia deseada.",
      "Sirva la sopa en cuencos y decore con perejil fresco o ramitas de tomillo."
    ],
    "notes_ingredients": "",
    "notes_instructions": ""
  }
]

turns-00069.parquet:36812

9fd75dbc19cccb0e856b3b4a
turn 1/1gpt-4.1-mini-2025-04-14EnglishSouth Korea688 words
degenerate_repetitionAbsentFinal dense release
USER
     Goal
    Traduire toutes les valeurs textuelles présentes dans une liste d’objets d’une recette de cuisine de en (ISO 639) vers de (ISO 639), en respectant le contexte culinaire.

    Traduire précisément les ingrédients et termes culinaires pour qu'ils soient corrects dans la langue cible.
    Maintenir la structure JSON intacte, en ne modifiant que les valeurs.
    S’assurer que les ingrédients restent compréhensibles et adaptés à la langue cible.

    Return Format
    Tu dois renvoyer une liste d’objets sous la forme reçu.
    - Chaque valeur doit être correctement traduite en de.
    - Les ingrédients doivent être adaptés à la langue cible.
    - Aucune clé ne doit être modifiée.

    Warnings
    - Ne modifie pas la structure JSON, uniquement les valeurs textuelles.
    - Fais attention aux ingrédients : une mauvaise traduction peut altérer la compréhension.
    - Ne laisse aucune valeur non traduite.
    - Ne renvoie que le JSON, sans explication.

    Context Dump
    Langue source : en (ISO 639)
    Langue cible : de (ISO 639)
    Données à traduire :

    [
  {
    "recipeCode": "affb2342dbf246f3bd871f999256b2ac",
    "updated": "2025-04-21T20:22:11.138Z",
    "title": "Mushroom Soup",
    "subtitle": "A simple and flavorful mushroom soup.",
    "description": "This recipe highlights the rich flavor of mushrooms. A blend of porcini and cremini mushrooms is recommended, but common button mushrooms can be substituted, though the flavor might be less intense. Feel free to personalize the recipe by experimenting with different mushroom varieties, herbs, and wines.",
    "ingredients": [
      {
        "section": "For the recipe",
        "ingredients": [
          "low-sodium chicken broth",
          "fresh thyme leaves or dried thyme",
          "potato, cubed",
          "olive oil and unsalted butter as needed",
          "chopped onion",
          "garlic, minced",
          "sliced mushrooms",
          "lemon juice",
          "freshly ground black pepper to taste",
          "red or white wine",
          "milk",
          "fresh parsley or thyme for garnish"
        ]
      }
    ],
    "instructions": [
      "Combine the chicken broth, thyme, and cubed potato in a saucepan. Simmer until the potato is tender.",
      "Using a blender or food processor, purée the mixture until smooth. Return the purée to the saucepan.",
      "In a sauté pan, heat a small amount of olive oil and unsalted butter over medium heat. Add the chopped onion, minced garlic, and sliced mushrooms.",
      "Sauté for several minutes, until the onions become translucent and the mushrooms are tender.",
      "Season with lemon juice and freshly ground black pepper to taste.",
      "Incorporate the sautéed mushroom mixture into the potato purée.",
      "Deglaze the sauté pan with the wine, stirring with a wooden spoon. Add the deglazing liquid to the combined soup mixture.",
      "Gently warm the soup over low heat while gradually adding milk to achieve the desired consistency.",
      "Ladle the soup into bowls and garnish with fresh parsley or thyme sprigs."
    ],
    "notes_ingredients": "",
    "notes_instructions": ""
  }
]

    
ASSISTANT
```json
[
  {
    "recipeCode": "affb2342dbf246f3bd871f999256b2ac",
    "updated": "2025-04-21T20:22:11.138Z",
    "title": "Pilzsuppe",
    "subtitle": "Eine einfache und geschmackvolle Pilzsuppe.",
    "description": "Dieses Rezept hebt den intensiven Geschmack von Pilzen hervor. Eine Mischung aus Steinpilzen und Cremini-Pilzen wird empfohlen, aber Champignons können als Ersatz verwendet werden, wobei der Geschmack weniger intensiv sein kann. Personalisieren Sie das Rezept gerne, indem Sie mit verschiedenen Pilzarten, Kräutern und Weinen experimentieren.",
    "ingredients": [
      {
        "section": "Für das Rezept",
        "ingredients": [
          "natriumreduzierte Hühnerbrühe",
          "frische Thymianblätter oder getrockneter Thymian",
          "Kartoffel, gewürfelt",
          "Olivenöl und ungesalzene Butter nach Bedarf",
          "gehackte Zwiebel",
          "Knoblauch, fein gehackt",
          "in Scheiben geschnittene Pilze",
          "Zitronensaft",
          "frisch gemahlener schwarzer Pfeffer nach Geschmack",
          "roter oder weißer Wein",
          "Milch",
          "frische Petersilie oder Thymian zum Garnieren"
        ]
      }
    ],
    "instructions": [
      "Hühnerbrühe, Thymian und gewürfelte Kartoffel in einem Topf kombinieren. Köcheln lassen, bis die Kartoffel weich ist.",
      "Mit einem Mixer oder einer Küchenmaschine die Mischung glatt pürieren. Das Püree zurück in den Topf geben.",
      "In einer Pfanne bei mittlerer Hitze etwas Olivenöl und ungesalzene Butter erhitzen. Gehackte Zwiebel, fein gehackten Knoblauch und in Scheiben geschnittene Pilze hinzufügen.",
      "Mehrere Minuten sautieren, bis die Zwiebeln glasig und die Pilze weich sind.",
      "Mit Zitronensaft und frisch gemahlenem schwarzem Pfeffer abschmecken.",
      "Die sautierte Pilzmischung unter das Kartoffelpüree rühren.",
      "Die Pfanne mit dem Wein ablöschen und mit einem Holzlöffel umrühren. Die Flüssigkeit zum Suppenmix geben.",
      "Die Suppe bei niedriger Hitze langsam erwärmen und nach und nach Milch hinzufügen, bis die gewünschte Konsistenz erreicht ist.",
      "Die Suppe in Schalen schöpfen und mit frischer Petersilie oder Thymianzweigen garnieren."
    ],
    "notes_ingredients": "",
    "notes_instructions": ""
  }
]
```

turns-00069.parquet:36813

298e75753b04686e1e814d0a
turn 1/1gpt-4.1-mini-2025-04-14Englishunknown country487 words
degenerate_repetitionAbsentFinal dense release
USER
     Goal
    Traduire toutes les valeurs textuelles présentes dans une liste d’objets d’une recette de cuisine de en (ISO 639) vers zh (ISO 639), en respectant le contexte culinaire.

    Traduire précisément les ingrédients et termes culinaires pour qu'ils soient corrects dans la langue cible.
    Maintenir la structure JSON intacte, en ne modifiant que les valeurs.
    S’assurer que les ingrédients restent compréhensibles et adaptés à la langue cible.

    Return Format
    Tu dois renvoyer une liste d’objets sous la forme reçu.
    - Chaque valeur doit être correctement traduite en zh.
    - Les ingrédients doivent être adaptés à la langue cible.
    - Aucune clé ne doit être modifiée.

    Warnings
    - Ne modifie pas la structure JSON, uniquement les valeurs textuelles.
    - Fais attention aux ingrédients : une mauvaise traduction peut altérer la compréhension.
    - Ne laisse aucune valeur non traduite.
    - Ne renvoie que le JSON, sans explication.

    Context Dump
    Langue source : en (ISO 639)
    Langue cible : zh (ISO 639)
    Données à traduire :

    [
  {
    "recipeCode": "affb2342dbf246f3bd871f999256b2ac",
    "updated": "2025-04-21T20:22:11.138Z",
    "title": "Mushroom Soup",
    "subtitle": "A simple and flavorful mushroom soup.",
    "description": "This recipe highlights the rich flavor of mushrooms. A blend of porcini and cremini mushrooms is recommended, but common button mushrooms can be substituted, though the flavor might be less intense. Feel free to personalize the recipe by experimenting with different mushroom varieties, herbs, and wines.",
    "ingredients": [
      {
        "section": "For the recipe",
        "ingredients": [
          "low-sodium chicken broth",
          "fresh thyme leaves or dried thyme",
          "potato, cubed",
          "olive oil and unsalted butter as needed",
          "chopped onion",
          "garlic, minced",
          "sliced mushrooms",
          "lemon juice",
          "freshly ground black pepper to taste",
          "red or white wine",
          "milk",
          "fresh parsley or thyme for garnish"
        ]
      }
    ],
    "instructions": [
      "Combine the chicken broth, thyme, and cubed potato in a saucepan. Simmer until the potato is tender.",
      "Using a blender or food processor, purée the mixture until smooth. Return the purée to the saucepan.",
      "In a sauté pan, heat a small amount of olive oil and unsalted butter over medium heat. Add the chopped onion, minced garlic, and sliced mushrooms.",
      "Sauté for several minutes, until the onions become translucent and the mushrooms are tender.",
      "Season with lemon juice and freshly ground black pepper to taste.",
      "Incorporate the sautéed mushroom mixture into the potato purée.",
      "Deglaze the sauté pan with the wine, stirring with a wooden spoon. Add the deglazing liquid to the combined soup mixture.",
      "Gently warm the soup over low heat while gradually adding milk to achieve the desired consistency.",
      "Ladle the soup into bowls and garnish with fresh parsley or thyme sprigs."
    ],
    "notes_ingredients": "",
    "notes_instructions": ""
  }
]

    
ASSISTANT
```json
[
  {
    "recipeCode": "affb2342dbf246f3bd871f999256b2ac",
    "updated": "2025-04-21T20:22:11.138Z",
    "title": "蘑菇汤",
    "subtitle": "一道简单又美味的蘑菇汤。",
    "description": "这道菜突出蘑菇的浓郁风味。推荐使用牛肝菌和褐蘑菇混合,但也可以用普通白蘑菇代替,虽然风味可能不那么浓郁。欢迎通过尝试不同种类的蘑菇、香草和葡萄酒来个性化这道菜。",
    "ingredients": [
      {
        "section": "配方所需",
        "ingredients": [
          "低钠鸡汤",
          "新鲜百里香叶或干百里香",
          "土豆,切块",
          "适量橄榄油和无盐黄油",
          "切碎的洋葱",
          "蒜末",
          "切片蘑菇",
          "柠檬汁",
          "现磨黑胡椒粉,适量",
          "红葡萄酒或白葡萄酒",
          "牛奶",
          "新鲜欧芹或百里香作装饰"
        ]
      }
    ],
    "instructions": [
      "将鸡汤、百里香和切块土豆放入锅中。小火煮至土豆变软。",
      "用搅拌机或食品料理机将混合物打成细腻的泥状。将泥状物倒回锅中。",
      "在煎锅中加热适量橄榄油和无盐黄油,中火加热。加入切碎的洋葱、蒜末和切片蘑菇。",
      "翻炒几分钟,直到洋葱变得半透明,蘑菇变软。",
      "加入柠檬汁和现磨黑胡椒调味。",
      "将炒好的蘑菇混合物加入土豆泥中搅拌均匀。",
      "用葡萄酒将煎锅进行脱酸处理,用木勺搅拌。将脱酸液加入汤中混合物里。",
      "用小火轻轻加热汤,同时逐渐加入牛奶,调节至理想的浓稠度。",
      "将汤舀入碗中,用新鲜欧芹或百里香枝装饰。"
    ],
    "notes_ingredients": "",
    "notes_instructions": ""
  }
]
```

turns-00069.parquet:36814

e0628a5246c064927dccca43
turn 1/2gpt-4.1-mini-2025-04-14FrenchDR Congo221 words
degenerate_repetitionAbsentFinal dense release
USER
Quel sont les articles de la constitution de la RDC qui militent pour la protection de l'environnement 
ASSISTANT
En République Démocratique du Congo (RDC), la Constitution comporte plusieurs articles qui militent pour la protection de l’environnement, reflétant l’importance accordée à la préservation des ressources naturelles et à la gestion durable de l’environnement. Voici les principaux articles concernés :

- **Article 53** : Cet article affirme que la loi garantit la protection, la conservation et la valorisation de l’environnement. Il souligne aussi que l’État doit assurer la gestion rationnelle des ressources naturelles dans l’intérêt des générations présentes et futures.

- **Article 54** : Cet article précise que toute personne a droit à un environnement sain et propice au développement. Il établit ainsi un droit fondamental lié à la qualité de l’environnement.

- **Article 206 alinéa 4** (dans la section relative aux compétences des provinces) : Les provinces ont des compétences en matière de protection de l’environnement, sous réserve des lois nationales, ce qui implique la décentralisation de cette responsabilité.

En résumé, la Constitution de la RDC reconnaît explicitement la protection de l’environnement comme un devoir de l’État et un droit du citoyen, établissant un cadre légal pour la gestion durable des ressources naturelles et la sauvegarde de l’environnement. Pour une application précise, ces articles sont complétés et développés par des lois spécifiques environnementales.

turns-00069.parquet:36815

62aa066439d4b4d2a1af687f
turn 2/2gpt-4.1-mini-2025-04-14FrenchDR Congo193 words
degenerate_repetitionAbsentFinal dense release
USER
Citez tout ses articles la qui militent pour la protection de l'environnement 
ASSISTANT
Bien sûr ! Voici les articles de la **Constitution de la République Démocratique du Congo** (adoptée en 2006) qui militent explicitement pour la protection de l’environnement :

---

### Article 53
**Protection de l'environnement**  
"La loi garantit la protection de l’environnement, la conservation et la valorisation des ressources naturelles. L’État veille à la gestion rationnelle des ressources naturelles dans l’intérêt des générations présentes et futures."

---

### Article 54  
**Droit à un environnement sain**  
"Toute personne a droit à un environnement sain et propice au développement."

---

### Article 206 (alinéa 4)  
**Compétence provinciale en matière d’environnement**  
"Les provinces sont compétentes en matière de protection de l’environnement, sans préjudice des lois nationales."

---

Outre ces articles, d’autres dispositions constitutionnelles peuvent avoir un impact indirect sur la gestion durable des ressources naturelles (notamment celles relatives aux droits fondamentaux, au bien-être et au développement durable), mais ce sont ces articles qui traitent spécifiquement de la protection de l’environnement.

---

Si vous souhaitez, je peux aussi vous fournir des références aux lois environnementales ou aux engagements internationaux de la RDC complétant ce cadre constitutionnel.

turns-00069.parquet:36816

cc643dc97e7e32fa13e99eb9
turn 1/3gpt-4.1-mini-2025-04-14PortugueseBrazil8 words
degenerate_repetitionAbsentFinal dense release
USER
hi
ASSISTANT
Hello! How can I assist you today?

turns-00069.parquet:36817

5317a264491f69391a32d681
turn 2/3gpt-4.1-mini-2025-04-14PortugueseBrazil2053 words
degenerate_repetitionAbsentFinal dense release
USER
está correto o código ?
# -*- coding: utf-8 -*-
import overpy
import json
import os
import requests
import json
import shapely.geometry
import math
from shapely.geometry import Point, shape
from shapely.geometry import shape, Point
from shapely.ops import unary_union
from math import sqrt
import time

# --- Funções Utilitárias ---
def baixar_dados_overpass(bbox):
    query = f"""
    [out:json][timeout:25];
    (
      way["highway"]({bbox});
    );
    out body;
    >;
    out skel qt;
    """
    url = "http://overpass-api.de/api/interpreter"
    response = requests.post(url, data=query)
    data = response.json()
    return data

def processar_dados(dados):
    nodes = {}
    ways = []
    for elemento in dados["elements"]:
        if elemento["type"] == "node":
            nodes[elemento["id"]] = elemento
        elif elemento["type"] == "way":
            ways.append(elemento)
    return nodes, ways

def encontrar_intersecoes(nodes, ways):
    node_count = {}
    ways_dict = {}

    for way in ways:
        for node_id in way["nodes"]:
            node_count[node_id] = node_count.get(node_id, 0) + 1
            if node_id not in ways_dict:
                ways_dict[node_id] = []
            ways_dict[node_id].append(way)

    intersecoes = {node_id: ways_dict[node_id] for node_id, count in node_count.items() if count > 1}
    return intersecoes

def angulo_entre_vetores(p1, p2, p3):
    """Calcula o ângulo entre 3 pontos: p1->p2->p3"""
    a = (p1[0] - p2[0], p1[1] - p2[1])
    b = (p3[0] - p2[0], p3[1] - p2[1])
    dot = a[0]*b[0] + a[1]*b[1]
    norm_a = math.hypot(*a)
    norm_b = math.hypot(*b)
    cos_theta = dot / (norm_a * norm_b + 1e-9)
    angle = math.degrees(math.acos(max(min(cos_theta, 1), -1)))
    return angle

def refinar_filtrar_intersecoes_v5(intersecoes, nodes, geo):
    intersecoes_validas = []

    for node_id, vias_conectadas in intersecoes.items():
        node = nodes[node_id]
        ponto = Point(node["lon"], node["lat"])

        if not geo.contains(ponto):
            continue

        nomes_vias = set()
        vias_info = []

        for via in vias_conectadas:
            tags = via.get("tags", {})
            tipo = tags.get("highway", "")
            nome = tags.get("name")
            junta = tags.get("junction", "")

            if tipo in ["footway", "path", "track", "service", "pedestrian"]:
                continue

            if junta == "roundabout":
                nome = f"rotatoria_{via['id']}"

            if nome:
                nomes_vias.add(nome)
            vias_info.append((via["id"], tipo, nome))

        if len(vias_info) < 2:
            continue

        if len(nomes_vias) <= 1:
            pontos_ao_redor = []
            for via in vias_conectadas:
                if node_id in via["nodes"]:
                    idx = via["nodes"].index(node_id)
                    nodes_via = via["nodes"]
                    if idx > 0 and idx < len(nodes_via) - 1:
                        p1 = nodes[nodes_via[idx - 1]]
                        p2 = nodes[node_id]
                        p3 = nodes[nodes_via[idx + 1]]
                        pontos_ao_redor.append((p1, p2, p3))

            angulos = []
            for p1, p2, p3 in pontos_ao_redor:
                ponto1 = (p1["lon"], p1["lat"])
                ponto2 = (p2["lon"], p2["lat"])
                ponto3 = (p3["lon"], p3["lat"])
                angulo = angulo_entre_vetores(ponto1, ponto2, ponto3)
                angulos.append(angulo)

            if all(angulo < 30 for angulo in angulos):
                continue

        intersecoes_validas.append((node_id, ponto))

    return intersecoes_validas

def main(bbox, geojson_area):
    print("🧪 Baixando dados da API Overpass...")
    dados = baixar_dados_overpass(bbox)
    print("✅ Dados recebidos. Processando...")

    nodes, ways = processar_dados(dados)
    print(f"🌐 Nós totais: {len(nodes)} | Vias totais: {len(ways)}")

    intersecoes = encontrar_intersecoes(nodes, ways)
    print(f"🔍 Interseções brutas encontradas: {len(intersecoes)}")

    area = shape(geojson_area["features"][0]["geometry"])
    intersecoes_refinadas = refinar_filtrar_intersecoes_v5(intersecoes, nodes, area)

    print(f"✅ Interseções REAIS refinadas: {len(intersecoes_refinadas)}")
    return intersecoes_refinadas
    
def arredondar_coord(lat, lon, casas=4):
    try: return (round(float(lat), casas), round(float(lon), casas))
    except: return None

def salvar_json(data, filepath):
    try:
        os.makedirs(os.path.dirname(filepath), exist_ok=True)
        with open(filepath, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"Arquivo JSON salvo: '{filepath}'")
        return True
    except Exception as e:
        print(f"Erro salvando JSON '{filepath}': {e}")
        return False

def listar_e_filtrar_geojson(geojson_path, distrito_nomes_desejados):
    print(f"\nAbrindo GeoJSON: {os.path.abspath(geojson_path)}")
    try:
        with open(geojson_path, 'r', encoding='utf-8') as f:
            geojson_data = json.load(f)
    except FileNotFoundError:
        print(f"*** ERRO: GeoJSON NÃO ENCONTRADO '{geojson_path}' ***")
        return None, None, None
    except Exception as e:
        print(f"ERRO lendo/parseando GeoJSON: {e}")
        return None, None, None

    if not isinstance(geojson_data, dict) or geojson_data.get("type") != "FeatureCollection":
        return None, None, None

    nomes_prop_ds_nome = set(
        f.get("properties", {}).get("ds_nome")
        for f in geojson_data.get("features", [])
        if f.get("properties", {}).get("ds_nome")
    )

    print("\n--- Nomes Distrito GeoJSON ('ds_nome') ---")
    [print(f"- {n}") for n in sorted(list(nomes_prop_ds_nome)) if n] or print("Nenhum nome encontrado.")
    print("----\n")

    nomes_encontrados_upper = {n.upper() for n in nomes_prop_ds_nome}
    distritos_desejados_set_upper = {name.upper() for name in distrito_nomes_desejados}
    features_filtradas = []
    geometrias_shapely = []
    distritos_processados = set()

    print(f"Filtrando por {len(distritos_desejados_set_upper)} distritos desejados...")
    for feature in geojson_data.get("features", []):
        nome = feature.get("properties", {}).get("ds_nome")
        if nome and nome.upper() in distritos_desejados_set_upper:
            distritos_processados.add(nome)
            features_filtradas.append(feature)
            try:
                geom = shape(feature['geometry'])
                geom = geom if geom.is_valid else geom.buffer(0)
                if geom.is_valid and not geom.is_empty:
                    geometrias_shapely.append(geom)
            except Exception as e:
                print(f"Aviso proc geom '{nome}': {e}")

    nomes_desejados_nao_encontrados = {
        name for name in distrito_nomes_desejados if name.upper() not in nomes_encontrados_upper
    }

    if nomes_desejados_nao_encontrados:
        print("\n!!!! ATENÇÃO: Distritos NÃO encontrados: !!!!\n - " + "\n - ".join(sorted(list(nomes_desejados_nao_encontrados))) + "\n-------------------------------------------\n")

    if not features_filtradas or not geometrias_shapely:
        print("ERRO CRÍTICO: Nenhum distrito processado.")
        return None, None, nomes_prop_ds_nome

    print(f"Processados {len(features_filtradas)} distritos com {len(geometrias_shapely)} geometrias.")
    geojson_filtrado = {
        "type": "FeatureCollection",
        "name": "distritos-filtrados",
        "crs": geojson_data.get("crs"),
        "features": features_filtradas
    }
    return geojson_filtrado, geometrias_shapely, nomes_prop_ds_nome

def calcular_bbox_geral(geometrias_shapely):
    if not geometrias_shapely:
        return None
    try:
        valid_geoms = [g for g in geometrias_shapely if g.is_valid and not g.is_empty]
        if not valid_geoms:
            return None
        uniao = unary_union(valid_geoms)
        margin = 0.0001
        if uniao.is_empty:
            return None
        min_lon, min_lat, max_lon, max_lat = uniao.bounds
        return (min_lon - margin, min_lat - margin, max_lon + margin, max_lat + margin)
    except Exception as e:
        print(f"Erro BBOX: {e}.")
        return None

def dividir_bbox(bbox, linhas=2, colunas=2):
    min_lon, min_lat, max_lon, max_lat = bbox
    h_step = (max_lat - min_lat) / linhas
    w_step = (max_lon - min_lon) / colunas
    sub_bboxes = []
    for i in range(linhas):
        s = min_lat + i * h_step
        n = min_lat + (i + 1) * h_step
        for j in range(colunas):
            w = min_lon + j * w_step
            e = min_lon + (j + 1) * w_step
            sub_bboxes.append((w, s, e, n))
    return sub_bboxes

def buscar_dados_overpass_sub_bbox(sub_bbox_coords, delay_segundos=5):
    bbox_overpass = f"{sub_bbox_coords[1]},{sub_bbox_coords[0]},{sub_bbox_coords[3]},{sub_bbox_coords[2]}"
    api = overpy.Overpass()
    highway_filter = '["highway"~"^(motorway|trunk|primary|secondary|tertiary|unclassified|residential|living_street)$"]'

    # ✅ QUERY MULTILINHA correta
    query = f"""
    [out:json][timeout:480];
    (
      way({bbox_overpass}){highway_filter};
    );
    out body;
    >;
    out skel qt;
    """

    print(f"\n  Consultando sub-BBOX: {bbox_overpass}")
    try:
        print(f"  Aguardando {delay_segundos}s...")
        time.sleep(delay_segundos)
        result = api.query(query)
        print(f"  Consulta sub-BBOX OK. Nós: {len(result.nodes)}, Ways: {len(result.ways)}.")

        for way in result.ways:
            for node in way.nodes:
                if not hasattr(node, 'way_ids'):
                    node.way_ids = []
                node.way_ids.append(str(way.id))

        return result
    except overpy.exception.OverpassError as e:
        print(f"  ERRO Overpass Query: {e}")
        [print(f"   -> API: {m}") for m in getattr(e, 'msgs', [])]
        return None
    except Exception as e:
        print(f"  ERRO Inesperado Overpass: {e}")
        return None

def refinar_filtrar_intersecoes_v5(nodes_candidatos, ways_dict, geometrias_validas):
    if not nodes_candidatos or not ways_dict or not geometrias_validas:
        return []

    intersecoes_refinadas = []
    print(f"\n[🔍] Refinando {len(nodes_candidatos)} nós candidatos (Lógica v5)...")

    try:
        area_unificada = unary_union([g for g in geometrias_validas if g.is_valid and not g.is_empty])
    except Exception as e:
        print(f"[⚠️] Erro unindo geometrias: {e}")
        return []

    stats = {'ok': 0, 'fora': 0, 'sem_nome': 0, 'rotatoria': 0, 'poucas_ruas': 0}

    for node in nodes_candidatos:
        try:
            ponto = Point(float(node.lon), float(node.lat))
            if not area_unificada.contains(ponto):
                stats['fora'] += 1
                continue

            if not hasattr(node, 'way_ids') or not node.way_ids:
                stats['poucas_ruas'] += 1
                continue

            nomes_ruas = set()
            ignorar = False

            for wid in node.way_ids:
                way = ways_dict.get(int(wid))
                if not way:
                    continue

                if way.tags.get("junction") == "roundabout":
                    ignorar = True
                    break

                nome = way.tags.get("name")
                if nome:
                    nomes_ruas.add(nome.strip())

            if ignorar:
                stats['rotatoria'] += 1
                continue

            if len(nomes_ruas) < 2:
                stats['sem_nome'] += 1
                continue

            intersecoes_refinadas.append(node)
            stats['ok'] += 1

        except Exception as e:
            print(f"[❌] Erro processando nó {getattr(node, 'id', '?')}: {e}")

    print("\n[✅] Estatísticas final do refino v5:")
    for k, v in stats.items():
        print(f" - {k.capitalize()}: {v}")
    return intersecoes_refinadas


def formatar_e_deduplicar_final(nodes_refinados, casas_decimais=4, distancia_minima=0.0003, offset_val=0.00015):
    if not nodes_refinados:
        return []
    
    finais = []
    marcadas = set()
    ultima_coord = None
    alternador = True  # True = deslocamento positivo, False = negativo

    print(f"\nFormatando e deduplicando {len(nodes_refinados)} interseções com offset alternado...")
    
    for node in nodes_refinados:
        try:
            lat, lon = float(node.lat), float(node.lon)
            latlon_original = arredondar_coord(lat, lon, casas=casas_decimais)
            if latlon_original is None or latlon_original in marcadas:
                continue

            if ultima_coord:
                dist = sqrt((lat - ultima_coord[0])**2 + (lon - ultima_coord[1])**2)
                if dist < distancia_minima:
                    continue  # muito próximo, pula

            # Alterna offset para evitar sobreposição visual
            if alternador:
                lat += offset_val
                lon -= offset_val
            else:
                lat -= offset_val
                lon += offset_val
            alternador = not alternador

            latlon_offset = arredondar_coord(lat, lon, casas=casas_decimais)
            if latlon_offset in marcadas:
                continue

            finais.append({
                "lat": lat,
                "lng": lon,
                "osm_node_id": int(node.id)
            })
            marcadas.add(latlon_offset)
            ultima_coord = (lat, lon)

        except Exception as e:
            print(f"Erro format/dedup nó {node.id}: {e}")
    
    print(f"Deduplicação com offset concluída. {len(finais)} interseções únicas.")
    return finais

# --- Execução Principal ---
INPUT_GEOJSON_PATH = "saopaulo_distritos_poligonos.geojson"
DISTRITOS_ZONA_LESTE = [
    "ARICANDUVA", "ARTUR ALVIM", "CANGAIBA", "CARRAO", "CIDADE LIDER", "CIDADE TIRADENTES",
    "ERMELINO MATARAZZO", "GUAIANASES", "IGUATEMI", "ITAIM PAULISTA", "ITAQUERA",
    "JARDIM HELENA", "JOSE BONIFACIO", "LAJEADO", "MOOCA", "PARQUE DO CARMO", "PENHA",
    "PONTE RASA", "SAPOPEMBA", "SAO LUCAS", "SAO MATEUS", "SAO MIGUEL", "SAO RAFAEL",
    "TATUAPE", "VILA CURUCA", "VILA FORMOSA", "VILA JACUI", "VILA MATILDE", "VILA PRUDENTE",
]

print(f"INFO: Usando {len(DISTRITOS_ZONA_LESTE)} distritos ZL.")
CASAS_DECIMAIS_DEDUP = 4
GRADE_LINHAS = 2
GRADE_COLUNAS = 2
DELAY_CONSULTAS = 5
OUTPUT_DIR = "outputs_refinado_v11"
OUTPUT_GEOJSON_FILTRADO_PATH = os.path.join(OUTPUT_DIR, "distritos_zona_leste_usados.geojson")
OUTPUT_INTERSECOES_PATH = os.path.join(OUTPUT_DIR, "intersecoes_zona_leste_final_v11.json")

print(f"Iniciando processo v11 (Query Fixa, Refino v4, Dedup={CASAS_DECIMAIS_DEDUP})...")
if not os.path.exists(OUTPUT_DIR):
    try:
        os.makedirs(OUTPUT_DIR)
        print(f"Diretório '{OUTPUT_DIR}' criado.")
    except OSError as e:
        print(f"ERRO CRÍTICO: Criar diretório '{OUTPUT_DIR}': {e}")
        exit()

geojson_zl_filtrado, geometrias_zl_shapely, _ = listar_e_filtrar_geojson(INPUT_GEOJSON_PATH, DISTRITOS_ZONA_LESTE)

if geometrias_zl_shapely:
    if geojson_zl_filtrado:
        salvar_json(geojson_zl_filtrado, OUTPUT_GEOJSON_FILTRADO_PATH)
    bbox_geral_zl = calcular_bbox_geral(geometrias_zl_shapely)
    if bbox_geral_zl:
        print(f"\nBBOX geral: {bbox_geral_zl}")
        sub_bboxes = dividir_bbox(bbox_geral_zl, linhas=GRADE_LINHAS, colunas=GRADE_COLUNAS)
        print(f"Dividido em {len(sub_bboxes)} sub-BBOXes ({GRADE_LINHAS}x{GRADE_COLUNAS}).")
        todos_nos_brutos = {}
        todas_ways_brutas = {}
        for i, sub_bbox in enumerate(sub_bboxes):
            print(f"\nProcessando sub-BBOX {i+1}/{len(sub_bboxes)}...")
            sub_result = buscar_dados_overpass_sub_bbox(sub_bbox, delay_segundos=DELAY_CONSULTAS)
            if sub_result and sub_result.nodes:
                for node in sub_result.nodes:
                    todos_nos_brutos[node.id] = node
                if hasattr(sub_result, 'ways') and sub_result.ways:
                    for way in sub_result.ways:
                        todas_ways_brutas[way.id] = way
            else:
                print(f"*** AVISO: Sub-BBOX {i+1} falhou ou retornou vazio. ***")

        if todos_nos_brutos:
            print(f"\nConsultas concluídas. {len(todos_nos_brutos)} nós únicos, {len(todas_ways_brutas)} ways únicas.")
            intersecoes_refinadas = refinar_filtrar_intersecoes_v5(
                list(todos_nos_brutos.values()), todas_ways_brutas, geometrias_zl_shapely
            )
            intersecoes_finais = formatar_e_deduplicar_final(
                intersecoes_refinadas, casas_decimais=CASAS_DECIMAIS_DEDUP
            )
            if intersecoes_finais:
                if salvar_json(intersecoes_finais, OUTPUT_INTERSECOES_PATH):
                    print(f"\n===> TOTAL FINAL: {len(intersecoes_finais)} interseções salvas em '{OUTPUT_INTERSECOES_PATH}'.")
                else:
                    print("\nERRO ao salvar JSON final.")
            else:
                print("\nNenhuma interseção final encontrada após refinamento/deduplicação.")
        else:
            print("\nNenhum nó encontrado nas consultas bem-sucedidas.")
    else:
        print("\nERRO CRÍTICO: Não calculou BBOX.")
else:
    print("\n!!! INTERROMPIDO: Falha ao processar GeoJSON !!!")

print("\nProcesso concluído.")
ASSISTANT
O código está bastante completo e bem estruturado para o propósito de baixar dados da API Overpass, processar e filtrar interseções de ruas dentro de uma área geográfica dada (zona leste de São Paulo, no seu exemplo).

De forma geral, sim, o código está correto e aparentemente funcional, com boas práticas como tratamento de exceção, uso do `shapely` para geometria e controle de taxa de query ao Overpass via delay. Porém, alguns pontos merecem destaque ou poderiam ser melhorados para maior clareza, eficiência e robustez:

---

### Pontos positivos:

- Uso de funções específicas e bem nomeadas que separam responsabilidades (download, processamento, filtragem, deduplicação).
- Uso do pacote `overpy` para query e parsing dos dados OSM, que é mais robusto do que tratar manualmente JSON.
- Verificação de inválidos e limpezas geométricas com `buffer(0)`.
- Controle de sobreposição geográfica (refinamento com ângulos entre vias).
- Log detalhado com prints informativos.
- Deduplicação com deslocamento alternado para evitar sobreposição visual.
- Verificação da existência do diretório e criação quando necessário.
- Tratamento para evitar ruas tipo "footway" e rotatórias (junction=roundabout).

---

### Pontos para revisão / melhorias potenciais:

1. **Importações repetidas**:
   ```python
   import json
   from shapely.geometry import Point, shape
   from shapely.geometry import shape, Point
   ```
   - `json` e as importações do mesmo módulo repetidas podem ser removidas para clareza.
   - Importações duplicadas de `Point` e `shape` podem ser unificadas numa linha só.

2. **Uso inconsistente de variáveis de coordenadas**:
   - Em algumas funções (exemplo: `refinar_filtrar_intersecoes_v5`), as coordenadas são passadas / usadas como `(lon, lat)`, em outras como `(lat, lon)`. Fique atento para manter um padrão (geralmente, OSM e shapely usam `(lon, lat)`).

3. **Comentários e documentação**:
   - Algumas funções poderiam ter docstrings detalhando parâmetros e retornos.
   - Comentários são úteis para indicar propósito de passos complexos (ex: o why do offset alternado).

4. **Tratamento de exceções genéricas**:
   - Vários blocos `try-except Exception as e:` capturam erros genéricos e continuam. Embora importante para robustez, pode esconder erros sutis.

5. **Uso do `1e-9` para evitar divisão por zero**:
   ```python
   cos_theta = dot / (norm_a * norm_b + 1e-9)
   ```
   - OK para evitar ZeroDivisionError, mas pode ser melhorável (ex: ignorar se vetores muito curtos).

6. **Naming: nomes mistos entre português e inglês**:
   - Ex: `baixar_dados_overpass`, `refinar_filtrar_intersecoes_v5` (português), mas depois `formatar_e_deduplicar_final` misturando português e inglês.
   - Consistência de idioma torna manutenção mais fácil.

7. **Reuso de função `refinar_filtrar_intersecoes_v5` em 2 versões diferentes no código**:
   - Aparece uma função `refinar_filtrar_intersecoes_v5(intersecoes, nodes, geo)` (que processa dict de interseções) e depois outra `refinar_filtrar_intersecoes_v5(nodes_candidatos, ways_dict, geometrias_validas)` (que processa objetos do `overpy`). Nomes iguais em escopos diferentes, pode causar confusão (funções definidas duas vezes acabam sobrescrevendo).

---

### Resumo

- O código deve funcionar como esperado, considerando que:
  - As dependências (`overpy`, `shapely`, `requests`) estejam instaladas.
  - O arquivo GeoJSON esteja corretamente formatado e presente.
  - O Overpass API responda dentro do timeout configurado.
- Recomendo:
  - Remover importações duplicadas.
  - Renomear as funções `refinar_filtrar_intersecoes_v5` para nomes distintos para evitar sobrescrita/confusão.
  - Padronizar o uso de coordenadas `(lon, lat)` onde possível.
  - Adicionar mais logs ou debug prints caso precise debugar.
  - Testar com pequenas áreas para validar fluxo.

Se desejar, posso ajudar a ajustar os pontos mencionados ou montar uma versão revisada e comentada!

---

Quer ajuda para fazer alguma dessas melhorias?

turns-00069.parquet:36818

ca77c8e904cfb0ccd8628699
turn 3/3gpt-4.1-mini-2025-04-14PortugueseBrazil1974 words
degenerate_repetitionAbsentFinal dense release
USER
arrume esse código para que as marcações fiquem exatamente nas interseções:
# -*- coding: utf-8 -*-
import overpy
import json
import os
import requests
import json
import shapely.geometry
import math
from shapely.geometry import Point, shape
from shapely.geometry import shape, Point
from shapely.ops import unary_union
from math import sqrt
import time

# --- Funções Utilitárias ---
def baixar_dados_overpass(bbox):
    query = f"""
    [out:json][timeout:25];
    (
      way["highway"]({bbox});
    );
    out body;
    >;
    out skel qt;
    """
    url = "http://overpass-api.de/api/interpreter"
    response = requests.post(url, data=query)
    data = response.json()
    return data

def processar_dados(dados):
    nodes = {}
    ways = []
    for elemento in dados["elements"]:
        if elemento["type"] == "node":
            nodes[elemento["id"]] = elemento
        elif elemento["type"] == "way":
            ways.append(elemento)
    return nodes, ways

def encontrar_intersecoes(nodes, ways):
    node_count = {}
    ways_dict = {}

    for way in ways:
        for node_id in way["nodes"]:
            node_count[node_id] = node_count.get(node_id, 0) + 1
            if node_id not in ways_dict:
                ways_dict[node_id] = []
            ways_dict[node_id].append(way)

    intersecoes = {node_id: ways_dict[node_id] for node_id, count in node_count.items() if count > 1}
    return intersecoes

def angulo_entre_vetores(p1, p2, p3):
    """Calcula o ângulo entre 3 pontos: p1->p2->p3"""
    a = (p1[0] - p2[0], p1[1] - p2[1])
    b = (p3[0] - p2[0], p3[1] - p2[1])
    dot = a[0]*b[0] + a[1]*b[1]
    norm_a = math.hypot(*a)
    norm_b = math.hypot(*b)
    cos_theta = dot / (norm_a * norm_b + 1e-9)
    angle = math.degrees(math.acos(max(min(cos_theta, 1), -1)))
    return angle

def refinar_filtrar_intersecoes_v5(intersecoes, nodes, geo):
    intersecoes_validas = []

    for node_id, vias_conectadas in intersecoes.items():
        node = nodes[node_id]
        ponto = Point(node["lon"], node["lat"])

        if not geo.contains(ponto):
            continue

        nomes_vias = set()
        vias_info = []

        for via in vias_conectadas:
            tags = via.get("tags", {})
            tipo = tags.get("highway", "")
            nome = tags.get("name")
            junta = tags.get("junction", "")

            if tipo in ["footway", "path", "track", "service", "pedestrian"]:
                continue

            if junta == "roundabout":
                nome = f"rotatoria_{via['id']}"

            if nome:
                nomes_vias.add(nome)
            vias_info.append((via["id"], tipo, nome))

        if len(vias_info) < 2:
            continue

        if len(nomes_vias) <= 1:
            pontos_ao_redor = []
            for via in vias_conectadas:
                if node_id in via["nodes"]:
                    idx = via["nodes"].index(node_id)
                    nodes_via = via["nodes"]
                    if idx > 0 and idx < len(nodes_via) - 1:
                        p1 = nodes[nodes_via[idx - 1]]
                        p2 = nodes[node_id]
                        p3 = nodes[nodes_via[idx + 1]]
                        pontos_ao_redor.append((p1, p2, p3))

            angulos = []
            for p1, p2, p3 in pontos_ao_redor:
                ponto1 = (p1["lon"], p1["lat"])
                ponto2 = (p2["lon"], p2["lat"])
                ponto3 = (p3["lon"], p3["lat"])
                angulo = angulo_entre_vetores(ponto1, ponto2, ponto3)
                angulos.append(angulo)

            if all(angulo < 30 for angulo in angulos):
                continue

        intersecoes_validas.append((node_id, ponto))

    return intersecoes_validas

def main(bbox, geojson_area):
    print("🧪 Baixando dados da API Overpass...")
    dados = baixar_dados_overpass(bbox)
    print("✅ Dados recebidos. Processando...")

    nodes, ways = processar_dados(dados)
    print(f"🌐 Nós totais: {len(nodes)} | Vias totais: {len(ways)}")

    intersecoes = encontrar_intersecoes(nodes, ways)
    print(f"🔍 Interseções brutas encontradas: {len(intersecoes)}")

    area = shape(geojson_area["features"][0]["geometry"])
    intersecoes_refinadas = refinar_filtrar_intersecoes_v5(intersecoes, nodes, area)

    print(f"✅ Interseções REAIS refinadas: {len(intersecoes_refinadas)}")
    return intersecoes_refinadas
    
def arredondar_coord(lat, lon, casas=4):
    try: return (round(float(lat), casas), round(float(lon), casas))
    except: return None

def salvar_json(data, filepath):
    try:
        os.makedirs(os.path.dirname(filepath), exist_ok=True)
        with open(filepath, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"Arquivo JSON salvo: '{filepath}'")
        return True
    except Exception as e:
        print(f"Erro salvando JSON '{filepath}': {e}")
        return False

def listar_e_filtrar_geojson(geojson_path, distrito_nomes_desejados):
    print(f"\nAbrindo GeoJSON: {os.path.abspath(geojson_path)}")
    try:
        with open(geojson_path, 'r', encoding='utf-8') as f:
            geojson_data = json.load(f)
    except FileNotFoundError:
        print(f"*** ERRO: GeoJSON NÃO ENCONTRADO '{geojson_path}' ***")
        return None, None, None
    except Exception as e:
        print(f"ERRO lendo/parseando GeoJSON: {e}")
        return None, None, None

    if not isinstance(geojson_data, dict) or geojson_data.get("type") != "FeatureCollection":
        return None, None, None

    nomes_prop_ds_nome = set(
        f.get("properties", {}).get("ds_nome")
        for f in geojson_data.get("features", [])
        if f.get("properties", {}).get("ds_nome")
    )

    print("\n--- Nomes Distrito GeoJSON ('ds_nome') ---")
    [print(f"- {n}") for n in sorted(list(nomes_prop_ds_nome)) if n] or print("Nenhum nome encontrado.")
    print("----\n")

    nomes_encontrados_upper = {n.upper() for n in nomes_prop_ds_nome}
    distritos_desejados_set_upper = {name.upper() for name in distrito_nomes_desejados}
    features_filtradas = []
    geometrias_shapely = []
    distritos_processados = set()

    print(f"Filtrando por {len(distritos_desejados_set_upper)} distritos desejados...")
    for feature in geojson_data.get("features", []):
        nome = feature.get("properties", {}).get("ds_nome")
        if nome and nome.upper() in distritos_desejados_set_upper:
            distritos_processados.add(nome)
            features_filtradas.append(feature)
            try:
                geom = shape(feature['geometry'])
                geom = geom if geom.is_valid else geom.buffer(0)
                if geom.is_valid and not geom.is_empty:
                    geometrias_shapely.append(geom)
            except Exception as e:
                print(f"Aviso proc geom '{nome}': {e}")

    nomes_desejados_nao_encontrados = {
        name for name in distrito_nomes_desejados if name.upper() not in nomes_encontrados_upper
    }

    if nomes_desejados_nao_encontrados:
        print("\n!!!! ATENÇÃO: Distritos NÃO encontrados: !!!!\n - " + "\n - ".join(sorted(list(nomes_desejados_nao_encontrados))) + "\n-------------------------------------------\n")

    if not features_filtradas or not geometrias_shapely:
        print("ERRO CRÍTICO: Nenhum distrito processado.")
        return None, None, nomes_prop_ds_nome

    print(f"Processados {len(features_filtradas)} distritos com {len(geometrias_shapely)} geometrias.")
    geojson_filtrado = {
        "type": "FeatureCollection",
        "name": "distritos-filtrados",
        "crs": geojson_data.get("crs"),
        "features": features_filtradas
    }
    return geojson_filtrado, geometrias_shapely, nomes_prop_ds_nome

def calcular_bbox_geral(geometrias_shapely):
    if not geometrias_shapely:
        return None
    try:
        valid_geoms = [g for g in geometrias_shapely if g.is_valid and not g.is_empty]
        if not valid_geoms:
            return None
        uniao = unary_union(valid_geoms)
        margin = 0.0001
        if uniao.is_empty:
            return None
        min_lon, min_lat, max_lon, max_lat = uniao.bounds
        return (min_lon - margin, min_lat - margin, max_lon + margin, max_lat + margin)
    except Exception as e:
        print(f"Erro BBOX: {e}.")
        return None

def dividir_bbox(bbox, linhas=2, colunas=2):
    min_lon, min_lat, max_lon, max_lat = bbox
    h_step = (max_lat - min_lat) / linhas
    w_step = (max_lon - min_lon) / colunas
    sub_bboxes = []
    for i in range(linhas):
        s = min_lat + i * h_step
        n = min_lat + (i + 1) * h_step
        for j in range(colunas):
            w = min_lon + j * w_step
            e = min_lon + (j + 1) * w_step
            sub_bboxes.append((w, s, e, n))
    return sub_bboxes

def buscar_dados_overpass_sub_bbox(sub_bbox_coords, delay_segundos=5):
    bbox_overpass = f"{sub_bbox_coords[1]},{sub_bbox_coords[0]},{sub_bbox_coords[3]},{sub_bbox_coords[2]}"
    api = overpy.Overpass()
    highway_filter = '["highway"~"^(motorway|trunk|primary|secondary|tertiary|unclassified|residential|living_street)$"]'

    # ✅ QUERY MULTILINHA correta
    query = f"""
    [out:json][timeout:480];
    (
      way({bbox_overpass}){highway_filter};
    );
    out body;
    >;
    out skel qt;
    """

    print(f"\n  Consultando sub-BBOX: {bbox_overpass}")
    try:
        print(f"  Aguardando {delay_segundos}s...")
        time.sleep(delay_segundos)
        result = api.query(query)
        print(f"  Consulta sub-BBOX OK. Nós: {len(result.nodes)}, Ways: {len(result.ways)}.")

        for way in result.ways:
            for node in way.nodes:
                if not hasattr(node, 'way_ids'):
                    node.way_ids = []
                node.way_ids.append(str(way.id))

        return result
    except overpy.exception.OverpassError as e:
        print(f"  ERRO Overpass Query: {e}")
        [print(f"   -> API: {m}") for m in getattr(e, 'msgs', [])]
        return None
    except Exception as e:
        print(f"  ERRO Inesperado Overpass: {e}")
        return None

def refinar_filtrar_intersecoes_v5(nodes_candidatos, ways_dict, geometrias_validas):
    if not nodes_candidatos or not ways_dict or not geometrias_validas:
        return []

    intersecoes_refinadas = []
    print(f"\n[🔍] Refinando {len(nodes_candidatos)} nós candidatos (Lógica v5)...")

    try:
        area_unificada = unary_union([g for g in geometrias_validas if g.is_valid and not g.is_empty])
    except Exception as e:
        print(f"[⚠️] Erro unindo geometrias: {e}")
        return []

    stats = {'ok': 0, 'fora': 0, 'sem_nome': 0, 'rotatoria': 0, 'poucas_ruas': 0}

    for node in nodes_candidatos:
        try:
            ponto = Point(float(node.lon), float(node.lat))
            if not area_unificada.contains(ponto):
                stats['fora'] += 1
                continue

            if not hasattr(node, 'way_ids') or not node.way_ids:
                stats['poucas_ruas'] += 1
                continue

            nomes_ruas = set()
            ignorar = False

            for wid in node.way_ids:
                way = ways_dict.get(int(wid))
                if not way:
                    continue

                if way.tags.get("junction") == "roundabout":
                    ignorar = True
                    break

                nome = way.tags.get("name")
                if nome:
                    nomes_ruas.add(nome.strip())

            if ignorar:
                stats['rotatoria'] += 1
                continue

            if len(nomes_ruas) < 2:
                stats['sem_nome'] += 1
                continue

            intersecoes_refinadas.append(node)
            stats['ok'] += 1

        except Exception as e:
            print(f"[❌] Erro processando nó {getattr(node, 'id', '?')}: {e}")

    print("\n[✅] Estatísticas final do refino v5:")
    for k, v in stats.items():
        print(f" - {k.capitalize()}: {v}")
    return intersecoes_refinadas


def formatar_e_deduplicar_final(nodes_refinados, casas_decimais=4, distancia_minima=0.0003, offset_val=0.00015):
    if not nodes_refinados:
        return []
    
    finais = []
    marcadas = set()
    ultima_coord = None
    alternador = True  # True = deslocamento positivo, False = negativo

    print(f"\nFormatando e deduplicando {len(nodes_refinados)} interseções com offset alternado...")
    
    for node in nodes_refinados:
        try:
            lat, lon = float(node.lat), float(node.lon)
            latlon_original = arredondar_coord(lat, lon, casas=casas_decimais)
            if latlon_original is None or latlon_original in marcadas:
                continue

            if ultima_coord:
                dist = sqrt((lat - ultima_coord[0])**2 + (lon - ultima_coord[1])**2)
                if dist < distancia_minima:
                    continue  # muito próximo, pula

            # Alterna offset para evitar sobreposição visual
            if alternador:
                lat += offset_val
                lon -= offset_val
            else:
                lat -= offset_val
                lon += offset_val
            alternador = not alternador

            latlon_offset = arredondar_coord(lat, lon, casas=casas_decimais)
            if latlon_offset in marcadas:
                continue

            finais.append({
                "lat": lat,
                "lng": lon,
                "osm_node_id": int(node.id)
            })
            marcadas.add(latlon_offset)
            ultima_coord = (lat, lon)

        except Exception as e:
            print(f"Erro format/dedup nó {node.id}: {e}")
    
    print(f"Deduplicação com offset concluída. {len(finais)} interseções únicas.")
    return finais

# --- Execução Principal ---
INPUT_GEOJSON_PATH = "saopaulo_distritos_poligonos.geojson"
DISTRITOS_ZONA_LESTE = [
    "ARICANDUVA", "ARTUR ALVIM", "CANGAIBA", "CARRAO", "CIDADE LIDER", "CIDADE TIRADENTES",
    "ERMELINO MATARAZZO", "GUAIANASES", "IGUATEMI", "ITAIM PAULISTA", "ITAQUERA",
    "JARDIM HELENA", "JOSE BONIFACIO", "LAJEADO", "MOOCA", "PARQUE DO CARMO", "PENHA",
    "PONTE RASA", "SAPOPEMBA", "SAO LUCAS", "SAO MATEUS", "SAO MIGUEL", "SAO RAFAEL",
    "TATUAPE", "VILA CURUCA", "VILA FORMOSA", "VILA JACUI", "VILA MATILDE", "VILA PRUDENTE",
]

print(f"INFO: Usando {len(DISTRITOS_ZONA_LESTE)} distritos ZL.")
CASAS_DECIMAIS_DEDUP = 4
GRADE_LINHAS = 2
GRADE_COLUNAS = 2
DELAY_CONSULTAS = 5
OUTPUT_DIR = "outputs_refinado_v11"
OUTPUT_GEOJSON_FILTRADO_PATH = os.path.join(OUTPUT_DIR, "distritos_zona_leste_usados.geojson")
OUTPUT_INTERSECOES_PATH = os.path.join(OUTPUT_DIR, "intersecoes_zona_leste_final_v11.json")

print(f"Iniciando processo v11 (Query Fixa, Refino v4, Dedup={CASAS_DECIMAIS_DEDUP})...")
if not os.path.exists(OUTPUT_DIR):
    try:
        os.makedirs(OUTPUT_DIR)
        print(f"Diretório '{OUTPUT_DIR}' criado.")
    except OSError as e:
        print(f"ERRO CRÍTICO: Criar diretório '{OUTPUT_DIR}': {e}")
        exit()

geojson_zl_filtrado, geometrias_zl_shapely, _ = listar_e_filtrar_geojson(INPUT_GEOJSON_PATH, DISTRITOS_ZONA_LESTE)

if geometrias_zl_shapely:
    if geojson_zl_filtrado:
        salvar_json(geojson_zl_filtrado, OUTPUT_GEOJSON_FILTRADO_PATH)
    bbox_geral_zl = calcular_bbox_geral(geometrias_zl_shapely)
    if bbox_geral_zl:
        print(f"\nBBOX geral: {bbox_geral_zl}")
        sub_bboxes = dividir_bbox(bbox_geral_zl, linhas=GRADE_LINHAS, colunas=GRADE_COLUNAS)
        print(f"Dividido em {len(sub_bboxes)} sub-BBOXes ({GRADE_LINHAS}x{GRADE_COLUNAS}).")
        todos_nos_brutos = {}
        todas_ways_brutas = {}
        for i, sub_bbox in enumerate(sub_bboxes):
            print(f"\nProcessando sub-BBOX {i+1}/{len(sub_bboxes)}...")
            sub_result = buscar_dados_overpass_sub_bbox(sub_bbox, delay_segundos=DELAY_CONSULTAS)
            if sub_result and sub_result.nodes:
                for node in sub_result.nodes:
                    todos_nos_brutos[node.id] = node
                if hasattr(sub_result, 'ways') and sub_result.ways:
                    for way in sub_result.ways:
                        todas_ways_brutas[way.id] = way
            else:
                print(f"*** AVISO: Sub-BBOX {i+1} falhou ou retornou vazio. ***")

        if todos_nos_brutos:
            print(f"\nConsultas concluídas. {len(todos_nos_brutos)} nós únicos, {len(todas_ways_brutas)} ways únicas.")
            intersecoes_refinadas = refinar_filtrar_intersecoes_v5(
                list(todos_nos_brutos.values()), todas_ways_brutas, geometrias_zl_shapely
            )
            intersecoes_finais = formatar_e_deduplicar_final(
                intersecoes_refinadas, casas_decimais=CASAS_DECIMAIS_DEDUP
            )
            if intersecoes_finais:
                if salvar_json(intersecoes_finais, OUTPUT_INTERSECOES_PATH):
                    print(f"\n===> TOTAL FINAL: {len(intersecoes_finais)} interseções salvas em '{OUTPUT_INTERSECOES_PATH}'.")
                else:
                    print("\nERRO ao salvar JSON final.")
            else:
                print("\nNenhuma interseção final encontrada após refinamento/deduplicação.")
        else:
            print("\nNenhum nó encontrado nas consultas bem-sucedidas.")
    else:
        print("\nERRO CRÍTICO: Não calculou BBOX.")
else:
    print("\n!!! INTERROMPIDO: Falha ao processar GeoJSON !!!")

print("\nProcesso concluído.")
ASSISTANT
Entendi, você está pedindo para ajustar o seu código para que as marcas (pontos) fiquem exatamente nas interseções, evitando deslocamentos artificiais ou erros de posicionamento.

---

### O que será ajustado

No seu código atual, na função `formatar_e_deduplicar_final` você aplica um deslocamento alternado (`offset_val`) para evitar que pontos fiquem sobrepostos um sobre o outro, o que acaba tirando a marcação da interseção exata.

Se você deseja que as marcações fiquem **exatamente nas interseções** (nas coordenadas reais do node OSM), você deve eliminar esse offset.

Além disso, o filtro de deduplicação com distância mínima pode ser mantido, mas talvez com valor menor para evitar remover pontos que são de interseções legítimas próximas.

---

### Abaixo está a versão ajustada da função `formatar_e_deduplicar_final` e algumas pequenas correções para garantir que as coordenadas salvam exatamente as interseções (sem deslocamento):

```python
def formatar_e_deduplicar_final(nodes_refinados, casas_decimais=6, distancia_minima=1e-5):
    """
    Recebe lista de nodes (objetos overpy.Node) e retorna lista de dicts com coordenadas exatas das interseções,
    removendo pontos duplicados próximos dentro de distancia_minima.
    Não aplica offsets para manter precisão na marcação.
    """
    if not nodes_refinados:
        return []

    finais = []
    marcadas = set()
    ultima_coord = None

    print(f"\nFormatando e deduplicando {len(nodes_refinados)} interseções sem deslocamento, precisão de {casas_decimais} casas decimais...")

    for node in nodes_refinados:
        try:
            lat, lon = float(node.lat), float(node.lon)
            latlon_rounded = (round(lat, casas_decimais), round(lon, casas_decimais))

            if latlon_rounded in marcadas:
                continue

            # Se quiser manter critério de distância mínima, descomente abaixo:
            # if ultima_coord:
            #     dist = sqrt((lat - ultima_coord[0])**2 + (lon - ultima_coord[1])**2)
            #     if dist < distancia_minima:
            #         continue  # muito próximo, pula

            finais.append({
                "lat": lat,
                "lng": lon,
                "osm_node_id": int(node.id)
            })
            marcadas.add(latlon_rounded)
            ultima_coord = (lat, lon)

        except Exception as e:
            print(f"Erro format/dedup nó {getattr(node, 'id', '?')}: {e}")

    print(f"Deduplicação concluída. {len(finais)} interseções únicas.")
    return finais
```

---

### Recomendações extras:

- No lugar de `casas_decimais=4`, recomendo aumentar para 6 casas decimais (~0.1m de precisão).
- No código principal, substitua a função atual pela acima para evitar deslocamentos artificiais.
- Caso a preocupação seja que pontos duplicados exatos existam (ex.: nodes OSM múltiplos muito proximos), aplicar deduplicação por distância mínima pequena é boa ideia (mas use valor pequeno).
- Você pode remover o parâmetro `offset_val` e o alternador do seu código.
- Não esqueça de também remover os imports duplicados que você tem.

---

### Resumo das modificações na sua pipeline:

1. Substitua a função `formatar_e_deduplicar_final` pela versão acima (sem offset).
2. Opcional: ajuste o parâmetro `casas_decimais` para 6.
3. Remova os trechos de código que aplicam offset na função, para que as coordenadas originais sejam mantidas.
4. Se preocupações com pontos duplicados próximos persistirem, ative a condição de distância mínima com valor pequeno (`1e-5` equivale a ~1 metro).

---

Se desejar, posso gerar o código completo do seu script já com essa alteração feita em contexto, só avisar!

turns-00069.parquet:36819

f785c0ed308c430629d9542f
turn 1/1gpt-4.1-mini-2025-04-14FrenchUnited Kingdom484 words
degenerate_repetitionAbsentFinal dense release
USER
     Goal
    Traduire toutes les valeurs textuelles présentes dans une liste d’objets d’une recette de cuisine de en (ISO 639) vers de (ISO 639), en respectant le contexte culinaire.

    Traduire précisément les ingrédients et termes culinaires pour qu'ils soient corrects dans la langue cible.
    Maintenir la structure JSON intacte, en ne modifiant que les valeurs.
    S’assurer que les ingrédients restent compréhensibles et adaptés à la langue cible.

    Return Format
    Tu dois renvoyer une liste d’objets sous la forme reçu.
    - Chaque valeur doit être correctement traduite en de.
    - Les ingrédients doivent être adaptés à la langue cible.
    - Aucune clé ne doit être modifiée.

    Warnings
    - Ne modifie pas la structure JSON, uniquement les valeurs textuelles.
    - Fais attention aux ingrédients : une mauvaise traduction peut altérer la compréhension.
    - Ne laisse aucune valeur non traduite.
    - Ne renvoie que le JSON, sans explication.

    Context Dump
    Langue source : en (ISO 639)
    Langue cible : de (ISO 639)
    Données à traduire :

    [
  {
    "recipeCode": "b2171632b50644c4a73f15c3527bc572",
    "updated": "2025-04-21T20:24:05.646Z",
    "title": "Anderson Family Steak Sauce",
    "subtitle": "This sauce pairs wonderfully with both chicken and steak. Stored in a sealed jar in the refrigerator, it will keep for up to two weeks.",
    "description": null,
    "ingredients": [
      {
        "section": "For the recipe",
        "ingredients": [
          "tomatoes, quartered",
          "olive oil",
          "shallot, minced",
          "white wine vinegar",
          "packed brown sugar",
          "salt-free pickle relish",
          "molasses",
          "garlic powder"
        ]
      }
    ],
    "instructions": [
      "Puree the quartered tomatoes in a blender.",
      "For an extra smooth sauce, strain the blended tomatoes through a fine-mesh sieve.",
      "In a pot, heat the olive oil over medium heat. Add the minced shallot and cook for several minutes, until softened.",
      "Combine the pureed tomatoes and all remaining ingredients in the pot. Bring the mixture to a simmer, then reduce the heat and cook for 2 1/2 to 3 hours.",
      "Let the sauce cool completely before refrigerating overnight.  Use the sauce the next day."
    ],
    "notes_ingredients": null,
    "notes_instructions": null
  }
]

    
ASSISTANT
```json
[
  {
    "recipeCode": "b2171632b50644c4a73f15c3527bc572",
    "updated": "2025-04-21T20:24:05.646Z",
    "title": "Anderson Familien-Steaksauce",
    "subtitle": "Diese Sauce passt wunderbar zu Hähnchen und Steak. In einem luftdicht verschlossenen Glas im Kühlschrank aufbewahrt, hält sie bis zu zwei Wochen.",
    "description": null,
    "ingredients": [
      {
        "section": "Für das Rezept",
        "ingredients": [
          "Tomaten, geviertelt",
          "Olivenöl",
          "Schalotte, fein gehackt",
          "Weißweinessig",
          "brauner Zucker, fest gepackt",
          "salzfreie Gewürzgurken-Relish",
          "Melasse",
          "Knoblauchpulver"
        ]
      }
    ],
    "instructions": [
      "Die geviertelten Tomaten in einem Mixer pürieren.",
      "Für eine extra glatte Sauce die pürierten Tomaten durch ein feines Sieb passieren.",
      "Olivenöl in einem Topf bei mittlerer Hitze erwärmen. Die fein gehackte Schalotte hinzufügen und mehrere Minuten anschwitzen, bis sie weich ist.",
      "Die pürierten Tomaten und alle übrigen Zutaten in den Topf geben. Die Mischung zum Kochen bringen, dann die Hitze reduzieren und 2 1/2 bis 3 Stunden köcheln lassen.",
      "Die Sauce vollständig abkühlen lassen, bevor sie über Nacht im Kühlschrank gelagert wird. Die Sauce am nächsten Tag verwenden."
    ],
    "notes_ingredients": null,
    "notes_instructions": null
  }
]
```

turns-00069.parquet:36820

28817c363fdecd134bee7974
turn 1/1gpt-4.1-mini-2025-04-14FrenchUnited States370 words
degenerate_repetitionAbsentFinal dense release
USER
     Goal
    Traduire toutes les valeurs textuelles présentes dans une liste d’objets d’une recette de cuisine de en (ISO 639) vers zh (ISO 639), en respectant le contexte culinaire.

    Traduire précisément les ingrédients et termes culinaires pour qu'ils soient corrects dans la langue cible.
    Maintenir la structure JSON intacte, en ne modifiant que les valeurs.
    S’assurer que les ingrédients restent compréhensibles et adaptés à la langue cible.

    Return Format
    Tu dois renvoyer une liste d’objets sous la forme reçu.
    - Chaque valeur doit être correctement traduite en zh.
    - Les ingrédients doivent être adaptés à la langue cible.
    - Aucune clé ne doit être modifiée.

    Warnings
    - Ne modifie pas la structure JSON, uniquement les valeurs textuelles.
    - Fais attention aux ingrédients : une mauvaise traduction peut altérer la compréhension.
    - Ne laisse aucune valeur non traduite.
    - Ne renvoie que le JSON, sans explication.

    Context Dump
    Langue source : en (ISO 639)
    Langue cible : zh (ISO 639)
    Données à traduire :

    [
  {
    "recipeCode": "b2171632b50644c4a73f15c3527bc572",
    "updated": "2025-04-21T20:24:05.646Z",
    "title": "Anderson Family Steak Sauce",
    "subtitle": "This sauce pairs wonderfully with both chicken and steak. Stored in a sealed jar in the refrigerator, it will keep for up to two weeks.",
    "description": null,
    "ingredients": [
      {
        "section": "For the recipe",
        "ingredients": [
          "tomatoes, quartered",
          "olive oil",
          "shallot, minced",
          "white wine vinegar",
          "packed brown sugar",
          "salt-free pickle relish",
          "molasses",
          "garlic powder"
        ]
      }
    ],
    "instructions": [
      "Puree the quartered tomatoes in a blender.",
      "For an extra smooth sauce, strain the blended tomatoes through a fine-mesh sieve.",
      "In a pot, heat the olive oil over medium heat. Add the minced shallot and cook for several minutes, until softened.",
      "Combine the pureed tomatoes and all remaining ingredients in the pot. Bring the mixture to a simmer, then reduce the heat and cook for 2 1/2 to 3 hours.",
      "Let the sauce cool completely before refrigerating overnight.  Use the sauce the next day."
    ],
    "notes_ingredients": null,
    "notes_instructions": null
  }
]

    
ASSISTANT
```json
[
  {
    "recipeCode": "b2171632b50644c4a73f15c3527bc572",
    "updated": "2025-04-21T20:24:05.646Z",
    "title": "安德森家族牛排汁",
    "subtitle": "这款酱汁非常适合搭配鸡肉和牛排。密封储存在冰箱中,可保存长达两周。",
    "description": null,
    "ingredients": [
      {
        "section": "食谱所需",
        "ingredients": [
          "番茄,切成四瓣",
          "橄榄油",
          "青葱,切末",
          "白葡萄酒醋",
          "紧实的红糖",
          "无盐泡菜调味酱",
          "糖蜜",
          "大蒜粉"
        ]
      }
    ],
    "instructions": [
      "将切成四瓣的番茄用搅拌机打成泥。",
      "为了获得更顺滑的酱汁,将搅拌好的番茄泥通过细网筛过滤。",
      "在锅中中火加热橄榄油。加入切末的青葱,煮几分钟至软化。",
      "将番茄泥和所有剩余的材料加入锅中。将混合物煮至微沸,然后调低火,慢炖2个半到3小时。",
      "让酱汁完全冷却后冷藏过夜。第二天使用酱汁。"
    ],
    "notes_ingredients": null,
    "notes_instructions": null
  }
]
```