fio-l1-paris-kml-pois

Status: done
Score: 100
Duration: 0:30
Cost: 0.53¢
Model: google/gemma-4-26b-a4b-it

Map (reference ← swipe → agent)

0:00
A colleague handed me `paris_late_night_pois`. Our internal map server only takes flat GeoJSON or GeoParquet, so please convert it to `paris_pois.geojson`, one row per placemark. Keep `name`, include the source's classification/grouping as a `category` column, and pull each entry's 'last verified' date out of the HTML blurb into a `verified_date` column as an ISO date. We want to flag stale records.
0:00
System info
{
  "adapter": "openrouter",
  "model": "google/gemma-4-26b-a4b-it",
  "base_url": "https://openrouter.ai/api/v1"
}
0:00
Bash ls -F
paris_late_night_pois.kml
0:01
Read paris_late_night_pois.kml
     1	<?xml version="1.0" encoding="UTF-8"?>
     2	<kml xmlns="http://www.opengis.net/kml/2.2">
     3	<Document>
     4	  <name>Late-night POIs (RATP night-bus study)</name>
     5	  <Folder>
     6	    <name>Cafés ouverts tard</name>
     7	    <Placemark>
     8	      <name>% Arabica</name>
     9	      <description><![CDATA[<b>% Arabica</b><br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="https://example.org/poi/0">Voir la fiche</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-01-01]]></description>
    10	      <Point>
    11	        <coordinates>2.324676,48.854156,0</coordinates>
    12	      </Point>
    13	    </Placemark>
    14	    <Placemark>
    15	      <name>1T. rue Scribe</name>
    16	      <description><![CDATA[<b>1T. rue Scribe</b><br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="https://example.org/poi/1">Voir la fiche</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-08-14]]></description>
    17	      <Point>
    18	        <coordinates>2.329774,48.870581,0</coordinates>
    19	      </Point>
    20	    </Placemark>
    21	    <Placemark>
    22	      <name>1ndix Café</name>
    23	      <description><![CDATA[<b>1ndix Café</b><br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="https://example.org/poi/2">Voir la fiche</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-03-27]]></description>
    24	      <Point>
    25	        <coordinates>2.341361,48.880764,0</coordinates>
    26	      </Point>
    27	    </Placemark>
    28	    <Placemark>
    29	      <name>2 Steps Café</name>
    30	      <description><![CDATA[<b>2 Steps Café</b><br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="https://example.org/poi/3">Voir la fiche</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-10-12]]></description>
    31	      <Point>
    32	        <coordinates>2.376626,48.866261,0</coordinates>
    33	      </Point>
    34	    </Placemark>
    35	    <Placemark>
    36	      <name>3234 Café - Trente Deux Trente Quatre</name>
    37	      <description><![CDATA[<b>3234 Café - Trente Deux Trente Quatre</b><br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="https://example.org/poi/4">Voir la fiche</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-05-25]]></description>
    38	      <Point>
    39	        <coordinates>2.376249,48.853167,0</coordinates>
    40	      </Point>
    41	    </Placemark>
    42	    <Placemark>
    43	      <name>360 Cafe</name>
    44	      <description><![CDATA[<b>360 Cafe</b><br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="https://example.org/poi/5">Voir la fiche</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-12-10]]></description>
    45	      <Point>
    46	        <coordinates>2.321838,48.842293,0</coordinates>
    47	      </Point>
    48	    </Placemark>
    49	    <Placemark>
    50	      <name>3w Cafe Paris</name>
    51	      <description><![CDATA[<b>3w Cafe Paris</b><br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="https://example.org/poi/6">Voir la fiche</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-07-23]]></description>
    52	      <Point>
    53	        <coordinates>2.327530,48.851560,0</coordinates>
    54	      </Point>
    55	    </Placemark>
    56	    <Placemark>
    57	      <name>48 Collagen Café</name>
    58	      <description><![CDATA[<b>48 Collagen Caf√©</b><br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="https://example.org/poi/7">Voir la fiche</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-02-08]]></description>
    59	      <Point>
    60	        <coordinates>2.340460,48.874840,0</coordinates>
    61	      </Point>
    62	    </Placemark>
    63	    <Placemark>
    64	      <name>86 Champs Elysées</name>
    65	      <description><![CDATA[<b>86 Champs Elysées</b><br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="https://example.org/poi/8">Voir la fiche</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-09-21]]></description>
    66	      <Point>
    67	        <coordinates>2.303767,48.871387,0</coordinates>
    68	      </Point>
    69	    </Placemark>
    70	    <Placemark>
    71	      <name>A la Ville de Paris</name>
    72	      <description><![CDATA[<b>A la Ville de Paris</b><br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="https://example.org/poi/9">Voir la fiche</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-04-06]]></description>
    73	      <Point>
    74	        <coordinates>2.344280,48.870530,0</coordinates>
    75	      </Point>
    76	    </Placemark>
    77	    <Placemark>
    78	      <name>A minima cafe</name>
    79	      <description><![CDATA[<b>A minima cafe</b><br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="https://example.org/poi/10">Voir la fiche</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-11-19]]></description>
    80	      <Point>
    81	        <coordinates>2.381290,48.845300,0</coordinates>
    82	      </Point>
    83	    </Placemark>
    84	    <Placemark>
    85	      <name>AD DictCafé</name>
    86	      <description><![CDATA[<b>AD DictCafé</b><br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="https://example.org/poi/11">Voir la fiche</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-06-04]]></description>
    87	      <Point>
    88	        <coordinates>2.390577,48.849002,0</coordinates>
    89	      </Point>
    90	    </Placemark>
    91	    <Placemark>
    92	      <name>ANGIOLO</name>
    93	      <description><![CDATA[<b>ANGIOLO</b><br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="https://example.org/poi/12">Voir la fiche</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-01-17]]></description>
    94	      <Point>
    95	        <coordinates>2.331682,48.854538,0</coordinates>
    96	      </Point>
    97	    </Placemark>
    98	    <Placemark>
    99	      <name>Abaita</name>
   100	      <description><![CDATA[<b>Abaita</b><br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="https://example.org/poi/13">Voir la fiche</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-08-02]]></description>
<system-reminder>Showed lines 1-100 of 330. Re-call Read with offset=101 to continue, or raise limit.</system-reminder>
0:10
Write solve.py
content · 71 lines · py
import geopandas as gpd
import pandas as pd
import re
from shapely.geometry import Point
import xml.etree.ElementTree as ET

def parse_kml(file_path):
    tree = ET.parse(file_path)
    root = tree.getroot()
    
    # KML namespaces
    ns = {'kml': 'http://www.opengis.net/kml/2.2'}
    
    features = []
    
    for placemark in root.findall('.//kml:Placemark', ns):
        name = placemark.find('kml:name', ns).text if placemark.find('kml:name', ns) is not None else None
        description = placemark.find('kml:description', ns).text if placemark.find('kml:description', ns) is not None else ""
        
        # Extract coordinates
        coords_text = placemark.find('.//kml:coordinates', ns).text.strip()
        # KML coords are lon,lat,alt
        lon, lat, _ = map(float, coords_text.split(','))
        
        # Parse description for category and verified_date
        # Example: <br/>Cat&eacute;gorie&nbsp;: Cafés ouverts tard<br/><a href="...">...</a><br/>Derni&egrave;re v&eacute;rification&nbsp;: 2026-01-01
        
        # Remove HTML tags to make regex easier, but be careful with entities
        # Let's use a more robust approach for the specific patterns
        
        # Category extraction: Look for "Catégorie : " pattern
        # Note: the description has HTML entities like &eacute; and &nbsp;
        # Let's clean the description first
        clean_desc = description.replace('&eacute;', 'e').replace('&egrave;', 'e').replace('&nbsp;', ' ')
        
        category = None
        category_match = re.search(r'Catégorie\s*:\s*([^<]+)', clean_desc)
        if category_match:
            category = category_match.group(1).strip()
            
        verified_date = None
        # Date extraction: Look for "Dernière vérification : YYYY-MM-DD"
        date_match = re.search(r'Dernière vérification\s*:\s*(\d{4}-\d{2}-\d{2})', clean_desc)
        if date_match:
            verified_date = date_match.group(1)
            
        features.append({
            'name': name,
            'category': category,
            'verified_date': verified_date,
            'geometry': Point(lon, lat)
        })
        
    return gpd.GeoDataFrame(features, crs="EPSG:4326")

if __name__ == "__main__":
    input_file = 'paris_late_night_pois.kml'
    output_file = 'paris_pois.geojson'
    
    gdf = parse_kml(input_file)
    
    # Ensure verified_date is ISO string (it should be from regex)
    # and name is present.
    
    # Filter to only keep requested columns
    gdf = gdf[['name', 'category', 'verified_date', 'geometry']]
    
    # Write to GeoJSON
    gdf.to_file(output_file, driver='GeoJSON')
    print(f"Successfully converted {len(gdf)} features to {output_file}")
Wrote solve.py (2726 chars).
0:11
Bash python solve.py
Successfully converted 45 features to paris_pois.geojson
0:15
Bash head -n 20 paris_pois.geojson
{
"type": "FeatureCollection",
"name": "paris_pois",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { "name": "% Arabica", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.324676, 48.854156 ] } },
{ "type": "Feature", "properties": { "name": "1T. rue Scribe", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.329774, 48.870581 ] } },
{ "type": "Feature", "properties": { "name": "1ndix Café", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.341361, 48.880764 ] } },
{ "type": "Feature", "properties": { "name": "2 Steps Café", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.376626, 48.866261 ] } },
{ "type": "Feature", "properties": { "name": "3234 Café - Trente Deux Trente Quatre", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.376249, 48.853167 ] } },
{ "type": "Feature", "properties": { "name": "360 Cafe", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.321838, 48.842293 ] } },
{ "type": "Feature", "properties": { "name": "3w Cafe Paris", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.32753, 48.85156 ] } },
{ "type": "Feature", "properties": { "name": "48 Collagen Café", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.34046, 48.87484 ] } },
{ "type": "Feature", "properties": { "name": "86 Champs Elysées", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.303767, 48.871387 ] } },
{ "type": "Feature", "properties": { "name": "A la Ville de Paris", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.34428, 48.87053 ] } },
{ "type": "Feature", "properties": { "name": "A minima cafe", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.38129, 48.8453 ] } },
{ "type": "Feature", "properties": { "name": "AD DictCafé", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.390577, 48.849002 ] } },
{ "type": "Feature", "properties": { "name": "ANGIOLO", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.331682, 48.854538 ] } },
{ "type": "Feature", "properties": { "name": "Abaita", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.368446, 48.864406 ] } },
{ "type": "Feature", "properties": { "name": "Absinthe Café", "category": null, "verified_date": null }, "geometry": { "type": "Point", "coordinates": [ 2.357228, 48.865564 ] } },
0:22
Write solve.py
content · 86 lines · py
import geopandas as gpd
import pandas as pd
import re
from shapely.geometry import Point
import xml.etree.ElementTree as ET
import html

def parse_kml(file_path):
    tree = ET.parse(file_path)
    root = tree.getroot()
    
    # KML namespaces
    ns = {'kml': 'http://www.opengis.net/kml/2.2'}
    
    features = []
    
    for placemark in root.findall('.//kml:Placemark', ns):
        name_elem = placemark.find('kml:name', ns)
        name = name_elem.text if name_elem is not None else None
        
        description_elem = placemark.find('kml:description', ns)
        description = description_elem.text if description_elem is not None else ""
        
        # Unescape HTML entities (like &eacute;)
        description = html.unescape(description)
        
        # Extract coordinates
        coords_elem = placemark.find('.//kml:coordinates', ns)
        if coords_elem is not None and coords_elem.text:
            coords_text = coords_elem.text.strip()
            # KML coords are lon,lat,alt
            parts = coords_text.split(',')
            lon = float(parts[0])
            lat = float(parts[1])
        else:
            continue
        
        # Use regex on the unescaped description
        # Category extraction: Look for "Catégorie : " pattern
        # Since we unescaped, we can look for 'Catégorie' directly.
        # The pattern in the KML is: Cat&eacute;gorie&nbsp;: Cafés ouverts tard
        # After unescape: Catégorie : Cafés ouverts tard
        
        category = None
        # Use \s* to handle potential non-breaking spaces or variations
        category_match = re.search(r'Catégorie\s*:\s*([^<]+)', description)
        if category_match:
            category = category_match.group(1).strip()
            
        verified_date = None
        # Date extraction: Look for "Dernière vérification : YYYY-MM-DD"
        # After unescape: Dernière vérification : 2026-01-01
        date_match = re.search(r'Dernière vérification\s*:\s*(\d{4}-\d{2}-\d{2})', description)
        if date_match:
            verified_date = date_match.group(1)
            
        features.append({
            'name': name,
            'category': category,
            'verified_date': verified_date,
            'geometry': Point(lon, lat)
        })
        
    return gpd.GeoDataFrame(features, crs="EPSG:4326")

if __name__ == "__main__":
    input_file = 'paris_late_night_pois.kml'
    output_file = 'paris_pois.geojson'
    
    gdf = parse_kml(input_file)
    
    # Ensure verified_date is ISO string (it should be from regex)
    # and name is present.
    
    # Filter to only keep requested columns
    gdf = gdf[['name', 'category', 'verified_date', 'geometry']]
    
    # Write to GeoJSON
    gdf.to_file(output_file, driver='GeoJSON')
    print(f"Successfully converted {len(gdf)} features to {output_file}")
    
    # Sanity check print
    if len(gdf) > 0:
        print("\nSample of processed data:")
        print(gdf[['name', 'category', 'verified_date']].head())
Wrote solve.py (3024 chars).
0:24
Bash python solve.py
Successfully converted 45 features to paris_pois.geojson

Sample of processed data:
                                    name            category verified_date
0                              % Arabica  Cafés ouverts tard    2026-01-01
1                         1T. rue Scribe  Cafés ouverts tard    2026-08-14
2                             1ndix Café  Cafés ouverts tard    2026-03-27
3                           2 Steps Café  Cafés ouverts tard    2026-10-12
4  3234 Café - Trente Deux Trente Quatre  Cafés ouverts tard    2026-05-25
0:26
Bash head -n 20 paris_pois.geojson
{
"type": "FeatureCollection",
"name": "paris_pois",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { "name": "% Arabica", "category": "Cafés ouverts tard", "verified_date": "2026-01-01" }, "geometry": { "type": "Point", "coordinates": [ 2.324676, 48.854156 ] } },
{ "type": "Feature", "properties": { "name": "1T. rue Scribe", "category": "Cafés ouverts tard", "verified_date": "2026-08-14" }, "geometry": { "type": "Point", "coordinates": [ 2.329774, 48.870581 ] } },
{ "type": "Feature", "properties": { "name": "1ndix Café", "category": "Cafés ouverts tard", "verified_date": "2026-03-27" }, "geometry": { "type": "Point", "coordinates": [ 2.341361, 48.880764 ] } },
{ "type": "Feature", "properties": { "name": "2 Steps Café", "category": "Cafés ouverts tard", "verified_date": "2026-10-12" }, "geometry": { "type": "Point", "coordinates": [ 2.376626, 48.866261 ] } },
{ "type": "Feature", "properties": { "name": "3234 Café - Trente Deux Trente Quatre", "category": "Cafés ouverts tard", "verified_date": "2026-05-25" }, "geometry": { "type": "Point", "coordinates": [ 2.376249, 48.853167 ] } },
{ "type": "Feature", "properties": { "name": "360 Cafe", "category": "Cafés ouverts tard", "verified_date": "2026-12-10" }, "geometry": { "type": "Point", "coordinates": [ 2.321838, 48.842293 ] } },
{ "type": "Feature", "properties": { "name": "3w Cafe Paris", "category": "Cafés ouverts tard", "verified_date": "2026-07-23" }, "geometry": { "type": "Point", "coordinates": [ 2.32753, 48.85156 ] } },
{ "type": "Feature", "properties": { "name": "48 Collagen Caf√©", "category": "Cafés ouverts tard", "verified_date": "2026-02-08" }, "geometry": { "type": "Point", "coordinates": [ 2.34046, 48.87484 ] } },
{ "type": "Feature", "properties": { "name": "86 Champs Elysées", "category": "Cafés ouverts tard", "verified_date": "2026-09-21" }, "geometry": { "type": "Point", "coordinates": [ 2.303767, 48.871387 ] } },
{ "type": "Feature", "properties": { "name": "A la Ville de Paris", "category": "Cafés ouverts tard", "verified_date": "2026-04-06" }, "geometry": { "type": "Point", "coordinates": [ 2.34428, 48.87053 ] } },
{ "type": "Feature", "properties": { "name": "A minima cafe", "category": "Cafés ouverts tard", "verified_date": "2026-11-19" }, "geometry": { "type": "Point", "coordinates": [ 2.38129, 48.8453 ] } },
{ "type": "Feature", "properties": { "name": "AD DictCafé", "category": "Cafés ouverts tard", "verified_date": "2026-06-04" }, "geometry": { "type": "Point", "coordinates": [ 2.390577, 48.849002 ] } },
{ "type": "Feature", "properties": { "name": "ANGIOLO", "category": "Cafés ouverts tard", "verified_date": "2026-01-17" }, "geometry": { "type": "Point", "coordinates": [ 2.331682, 48.854538 ] } },
{ "type": "Feature", "properties": { "name": "Abaita", "category": "Cafés ouverts tard", "verified_date": "2026-08-02" }, "geometry": { "type": "Point", "coordinates": [ 2.368446, 48.864406 ] } },
{ "type": "Feature", "properties": { "name": "Absinthe Café", "category": "Cafés ouverts tard", "verified_date": "2026-03-15" }, "geometry": { "type": "Point", "coordinates": [ 2.357228, 48.865564 ] } },
0:29