fio-l1-paris-kml-pois
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égorie : Cafés ouverts tard<br/><a href="https://example.org/poi/0">Voir la fiche</a><br/>Dernière vérification : 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égorie : Cafés ouverts tard<br/><a href="https://example.org/poi/1">Voir la fiche</a><br/>Dernière vérification : 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égorie : Cafés ouverts tard<br/><a href="https://example.org/poi/2">Voir la fiche</a><br/>Dernière vérification : 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égorie : Cafés ouverts tard<br/><a href="https://example.org/poi/3">Voir la fiche</a><br/>Dernière vérification : 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égorie : Cafés ouverts tard<br/><a href="https://example.org/poi/4">Voir la fiche</a><br/>Dernière vérification : 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égorie : Cafés ouverts tard<br/><a href="https://example.org/poi/5">Voir la fiche</a><br/>Dernière vérification : 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égorie : Cafés ouverts tard<br/><a href="https://example.org/poi/6">Voir la fiche</a><br/>Dernière vérification : 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égorie : Cafés ouverts tard<br/><a href="https://example.org/poi/7">Voir la fiche</a><br/>Dernière vérification : 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égorie : Cafés ouverts tard<br/><a href="https://example.org/poi/8">Voir la fiche</a><br/>Dernière vérification : 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égorie : Cafés ouverts tard<br/><a href="https://example.org/poi/9">Voir la fiche</a><br/>Dernière vérification : 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égorie : Cafés ouverts tard<br/><a href="https://example.org/poi/10">Voir la fiche</a><br/>Dernière vérification : 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égorie : Cafés ouverts tard<br/><a href="https://example.org/poi/11">Voir la fiche</a><br/>Dernière vérification : 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égorie : Cafés ouverts tard<br/><a href="https://example.org/poi/12">Voir la fiche</a><br/>Dernière vérification : 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égorie : Cafés ouverts tard<br/><a href="https://example.org/poi/13">Voir la fiche</a><br/>Dernière vérification : 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égorie : Cafés ouverts tard<br/><a href="...">...</a><br/>Dernière vérification : 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 é and
# Let's clean the description first
clean_desc = description.replace('é', 'e').replace('è', 'e').replace(' ', ' ')
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 é)
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égorie : 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