<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: geospatial</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/geospatial.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-03-16T20:12:32+00:00</updated><author><name>Simon Willison</name></author><entry><title>Coding agents for data analysis</title><link href="https://simonwillison.net/2026/Mar/16/coding-agents-for-data-analysis/#atom-tag" rel="alternate"/><published>2026-03-16T20:12:32+00:00</published><updated>2026-03-16T20:12:32+00:00</updated><id>https://simonwillison.net/2026/Mar/16/coding-agents-for-data-analysis/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/"&gt;Coding agents for data analysis&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Here's the handout I prepared for my NICAR 2026 workshop "Coding agents for data analysis" - a three hour session aimed at data journalists demonstrating ways that tools like Claude Code and OpenAI Codex can be used to explore, analyze and clean data.&lt;/p&gt;
&lt;p&gt;Here's the table of contents:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/coding-agents.html"&gt;Coding agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/warmup.html"&gt;Warmup: ChatGPT and Claude&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/setup.html"&gt;Setup Claude Code and Codex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/asking-questions.html"&gt;Asking questions against a database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/exploring-data.html"&gt;Exploring data with agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/cleaning-trees.html"&gt;Cleaning data: decoding neighborhood codes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/visualizations.html"&gt;Creating visualizations with agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/scraping.html"&gt;Scraping data with agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I ran the workshop using GitHub Codespaces and OpenAI Codex, since it was easy (and inexpensive) to distribute a budget-restricted API key for Codex that attendees could use during the class. Participants ended up burning $23 of Codex tokens.&lt;/p&gt;
&lt;p&gt;The exercises all used Python and SQLite and some of them used Datasette.&lt;/p&gt;
&lt;p&gt;One highlight of the workshop was when we started &lt;a href="https://simonw.github.io/nicar-2026-coding-agents/visualizations.html#javascript-visualizations"&gt;running Datasette&lt;/a&gt; such that it served static content from a &lt;code&gt;viz/&lt;/code&gt; folder, then had Claude Code start vibe coding new interactive visualizations directly in that folder. Here's a heat map it created for my trees database using Leaflet and &lt;a href="https://github.com/Leaflet/Leaflet.heat"&gt;Leaflet.heat&lt;/a&gt;, &lt;a href="https://gist.github.com/simonw/985ae2a6a3cd3df3fd375eb58dabea0f"&gt;source code here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of a &amp;quot;Trees SQL Map&amp;quot; web application with the heading &amp;quot;Trees SQL Map&amp;quot; and subheading &amp;quot;Run a query and render all returned points as a heat map. The default query targets roughly 200,000 trees.&amp;quot; Below is an input field containing &amp;quot;/trees/-/query.json&amp;quot;, a &amp;quot;Run Query&amp;quot; button, and a SQL query editor with the text &amp;quot;SELECT cast(Latitude AS float) AS latitude, cast(Longitude AS float) AS longitude, CASE WHEN DBH IS NULL OR DBH = '' THEN 0.3 WHEN cast(DBH AS float) &amp;lt;= 0 THEN 0.3 WHEN cast(DBH AS float) &amp;gt;= 80 THEN 1.0&amp;quot; (query is truncated). A status message reads &amp;quot;Loaded 1,000 rows and plotted 1,000 points as heat map.&amp;quot; Below is a Leaflet/OpenStreetMap interactive map of San Francisco showing a heat map overlay of tree locations, with blue/green clusters concentrated in areas like the Richmond District, Sunset District, and other neighborhoods. Map includes zoom controls and a &amp;quot;Leaflet | © OpenStreetMap contributors&amp;quot; attribution." src="https://static.simonwillison.net/static/2026/tree-sql-map.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;I designed the handout to also be useful for people who weren't able to attend the session in person. As is usually the case, material aimed at data journalists is equally applicable to anyone else with data to explore.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/data-journalism"&gt;data-journalism&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/speaking"&gt;speaking&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-codespaces"&gt;github-codespaces&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nicar"&gt;nicar&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/codex"&gt;codex&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;



</summary><category term="data-journalism"/><category term="geospatial"/><category term="python"/><category term="speaking"/><category term="sqlite"/><category term="ai"/><category term="datasette"/><category term="generative-ai"/><category term="llms"/><category term="github-codespaces"/><category term="nicar"/><category term="coding-agents"/><category term="claude-code"/><category term="codex"/><category term="leaflet"/></entry><entry><title>Let the LLM Write the Prompts: An Intro to DSPy in Compound Al Pipelines</title><link href="https://simonwillison.net/2025/Oct/4/drew-on-dspy/#atom-tag" rel="alternate"/><published>2025-10-04T22:48:59+00:00</published><updated>2025-10-04T22:48:59+00:00</updated><id>https://simonwillison.net/2025/Oct/4/drew-on-dspy/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.youtube.com/watch?v=I9ZtkgYZnOw"&gt;Let the LLM Write the Prompts: An Intro to DSPy in Compound Al Pipelines&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I've had trouble getting my head around &lt;a href="https://dspy.ai"&gt;DSPy&lt;/a&gt; in the past. This half hour talk by Drew Breunig at the recent Databricks Data + AI Summit is the clearest explanation I've seen yet of the kinds of problems it can help solve.&lt;/p&gt;
&lt;p&gt;Here's Drew's &lt;a href="https://www.dbreunig.com/2025/06/10/let-the-model-write-the-prompt.html"&gt;written version of the talk&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Drew works on Overture Maps, which combines Point Of Interest data from numerous providers to create a single unified POI database. This is an example of &lt;strong&gt;conflation&lt;/strong&gt;, a notoriously difficult task in GIS where multiple datasets are deduped and merged together.&lt;/p&gt;
&lt;p&gt;Drew uses an inexpensive local model, &lt;a href="https://huggingface.co/Qwen/Qwen3-0.6B"&gt;Qwen3-0.6B&lt;/a&gt;, to compare 70 million addresses and identity matches, for example between &lt;code&gt;Place(address="3359 FOOTHILL BLVD", name="RESTAURANT LOS ARCOS")&lt;/code&gt; and &lt;code&gt;Place(address="3359 FOOTHILL BLVD", name="Los Arcos Taqueria"')&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;DSPy's role is to optimize the prompt used for that smaller model. Drew used GPT-4.1 and the &lt;a href="https://dspy.ai/api/optimizers/MIPROv2/"&gt;dspy.MIPROv2&lt;/a&gt; optimizer, producing a 700 token prompt that increased the score from 60.7% to 82%.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Determine if two points of interest refer to the same place. Arrow to optimized prompt: Given two records representing places or businesses-each with at least a name and address-analyze the information and determine if they refer to the same real-world entity. Consider minor differences such as case, diacritics, transliteration, abbreviations, or formatting as potential matches if both the name and address are otherwise strongly similar. Only output &amp;quot;True&amp;quot; if both fields are a close match; if there are significant differences in either the name or address, even if one field matches exactly, output &amp;quot;False&amp;quot;. Your decision should be robust to common variations and errors and should work across multiple languages and scripts." src="https://static.simonwillison.net/static/2025/optimized-prompt.jpeg" /&gt;&lt;/p&gt;
&lt;p&gt;Why bother? Drew points out that having a prompt optimization pipeline makes it trivial to evaluate and switch to other models if they can score higher with a custom optimized prompt - without needing to execute that trial-and-error optimization by hand.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prompt-engineering"&gt;prompt-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/drew-breunig"&gt;drew-breunig&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/overture"&gt;overture&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dspy"&gt;dspy&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="ai"/><category term="prompt-engineering"/><category term="generative-ai"/><category term="llms"/><category term="drew-breunig"/><category term="overture"/><category term="dspy"/></entry><entry><title>Spatial Joins in DuckDB</title><link href="https://simonwillison.net/2025/Aug/23/spatial-joins-in-duckdb/#atom-tag" rel="alternate"/><published>2025-08-23T21:21:02+00:00</published><updated>2025-08-23T21:21:02+00:00</updated><id>https://simonwillison.net/2025/Aug/23/spatial-joins-in-duckdb/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://duckdb.org/2025/08/08/spatial-joins"&gt;Spatial Joins in DuckDB&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Extremely detailed overview by Max Gabrielsson of DuckDB's new spatial join optimizations.&lt;/p&gt;
&lt;p&gt;Consider the following query, which counts the number of &lt;a href="https://citibikenyc.com/system-data"&gt;NYC Citi Bike Trips&lt;/a&gt; for each of the neighborhoods defined by the &lt;a href="https://www.nyc.gov/content/planning/pages/resources/datasets/neighborhood-tabulation"&gt;NYC Neighborhood Tabulation Areas polygons&lt;/a&gt; and returns the top three:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;SELECT&lt;/span&gt; neighborhood,
  &lt;span class="pl-c1"&gt;count&lt;/span&gt;(&lt;span class="pl-k"&gt;*&lt;/span&gt;) &lt;span class="pl-k"&gt;AS&lt;/span&gt; num_rides
&lt;span class="pl-k"&gt;FROM&lt;/span&gt; rides
&lt;span class="pl-k"&gt;JOIN&lt;/span&gt; hoods &lt;span class="pl-k"&gt;ON&lt;/span&gt; ST_Intersects(
  &lt;span class="pl-c1"&gt;rides&lt;/span&gt;.&lt;span class="pl-c1"&gt;start_geom&lt;/span&gt;, &lt;span class="pl-c1"&gt;hoods&lt;/span&gt;.&lt;span class="pl-c1"&gt;geom&lt;/span&gt;
)
&lt;span class="pl-k"&gt;GROUP BY&lt;/span&gt; neighborhood
&lt;span class="pl-k"&gt;ORDER BY&lt;/span&gt; num_rides &lt;span class="pl-k"&gt;DESC&lt;/span&gt;
&lt;span class="pl-k"&gt;LIMIT&lt;/span&gt; &lt;span class="pl-c1"&gt;3&lt;/span&gt;;&lt;/pre&gt;

&lt;p&gt;The rides table contains 58,033,724 rows. The hoods table has polygons for 310 neighborhoods.&lt;/p&gt;
&lt;p&gt;Without an optimized spatial joins this query requires a nested loop join, executing that expensive &lt;code&gt;ST_Intersects()&lt;/code&gt; operation 58m * 310 ~= 18 billion times. This took around 30 minutes on the 36GB MacBook M3 Pro used for the benchmark.&lt;/p&gt;
&lt;p&gt;The first optimization described - implemented from DuckDB 1.2.0 onwards - uses a "piecewise merge join". This takes advantage of the fact that a bounding box intersection is a whole lot faster to calculate, especially if you pre-cache the bounding box (aka the minimum bounding rectangle or MBR) in the stored binary &lt;code&gt;GEOMETRY&lt;/code&gt; representation.&lt;/p&gt;
&lt;p&gt;Rewriting the query to use a fast bounding box intersection and then only running the more expensive &lt;code&gt;ST_Intersects()&lt;/code&gt; filters on those matches drops the runtime from 1800 seconds to 107 seconds.&lt;/p&gt;
&lt;p&gt;The second optimization, added in &lt;a href="https://duckdb.org/2025/05/21/announcing-duckdb-130.html"&gt;DuckDB 1.3.0&lt;/a&gt; in May 2025 using the new SPATIAL_JOIN operator, is significantly more sophisticated.&lt;/p&gt;
&lt;p&gt;DuckDB can now identify when a spatial join is working against large volumes of data and automatically build an in-memory R-Tree of bounding boxes for the larger of the two tables being joined.&lt;/p&gt;
&lt;p&gt;This new R-Tree further accelerates the bounding box intersection part of the join, and drops the runtime down to just 30 seconds.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://bsky.app/profile/mackaszechno.bsky.social/post/3lx3lnagg7s2t"&gt;@mackaszechno.bsky.social&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sql"&gt;sql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/duckdb"&gt;duckdb&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="sql"/><category term="duckdb"/></entry><entry><title>I counted all of the yurts in Mongolia using machine learning</title><link href="https://simonwillison.net/2025/Jun/18/yurts-in-mongolia/#atom-tag" rel="alternate"/><published>2025-06-18T14:11:33+00:00</published><updated>2025-06-18T14:11:33+00:00</updated><id>https://simonwillison.net/2025/Jun/18/yurts-in-mongolia/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://monroeclinton.com/counting-all-yurts-in-mongolia/"&gt;I counted all of the yurts in Mongolia using machine learning&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Fascinating, detailed account by Monroe Clinton of a geospatial machine learning project. Monroe wanted to count visible yurts in Mongolia using Google Maps satellite view. The resulting project incorporates &lt;a href="https://github.com/mapbox/mercantile"&gt;mercantile&lt;/a&gt; for tile calculations, &lt;a href="https://labelstud.io/"&gt;Label Studio&lt;/a&gt; for help label the first 10,000 examples, a model trained on top of &lt;a href="https://github.com/ultralytics/ultralytics"&gt;YOLO11&lt;/a&gt; and a bunch of clever custom Python code to co-ordinate a brute force search across 120 CPU workers running the model.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=44307629"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/machine-learning"&gt;machine-learning&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="machine-learning"/><category term="python"/><category term="ai"/></entry><entry><title>DuckDB is Probably the Most Important Geospatial Software of the Last Decade</title><link href="https://simonwillison.net/2025/May/4/duckdb-is-probably-the-most-important-geospatial-software-of-the/#atom-tag" rel="alternate"/><published>2025-05-04T00:28:35+00:00</published><updated>2025-05-04T00:28:35+00:00</updated><id>https://simonwillison.net/2025/May/4/duckdb-is-probably-the-most-important-geospatial-software-of-the/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.dbreunig.com/2025/05/03/duckdb-is-the-most-impactful-geospatial-software-in-a-decade.html"&gt;DuckDB is Probably the Most Important Geospatial Software of the Last Decade&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Drew Breunig argues that the ease of installation of DuckDB is opening up geospatial analysis to a whole new set of developers.&lt;/p&gt;
&lt;p&gt;This inspired &lt;a href="https://news.ycombinator.com/item?id=43881468#43882914"&gt;a comment on Hacker News&lt;/a&gt; from DuckDB Labs geospatial engineer Max Gabrielsson which helps explain why the drop in friction introduced by DuckDB is so significant:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I think a big part is that duckdbs spatial extension provides a SQL interface to a whole suite of standard foss gis packages by statically bundling everything (including inlining the default PROJ database of coordinate projection systems into the binary) and providing it for multiple platforms (including WASM). I.E there are no transitive dependencies except libc.&lt;/p&gt;
&lt;p&gt;[...] the fact that you can e.g. convert too and from a myriad of different geospatial formats by utilizing GDAL, transforming through SQL, or pulling down the latest overture dump without having the whole workflow break just cause you updated QGIS has probably been the main killer feature for a lot of the early adopters.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I've lost count of the time I've spent fiddling with dependencies like GDAL trying to get various geospatial tools to work in the past. Bundling difficult dependencies statically is an under-appreciated trick!&lt;/p&gt;
&lt;p&gt;If the bold claim in the headline inspires you to provide a counter-example, bear in mind that a decade ago is 2015, and most of the key technologies
In the modern geospatial stack - QGIS, PostGIS, geopandas, SpatiaLite - predate that by quite a bit.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sql"&gt;sql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/duckdb"&gt;duckdb&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/drew-breunig"&gt;drew-breunig&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="sql"/><category term="duckdb"/><category term="drew-breunig"/></entry><entry><title>The GeoGuessr StreetView meta-game</title><link href="https://simonwillison.net/2025/Apr/26/geoguessr/#atom-tag" rel="alternate"/><published>2025-04-26T16:56:59+00:00</published><updated>2025-04-26T16:56:59+00:00</updated><id>https://simonwillison.net/2025/Apr/26/geoguessr/#atom-tag</id><summary type="html">
    &lt;p&gt;My post on &lt;a href="https://simonwillison.net/2025/Apr/26/o3-photo-locations/"&gt;o3 guessing locations from photos&lt;/a&gt; made it &lt;a href="https://news.ycombinator.com/item?id=43803243"&gt;to Hacker News&lt;/a&gt; and by far the most interesting comments are from SamPatt, a self-described competitive &lt;a href="https://www.geoguessr.com/"&gt;GeoGuessr&lt;/a&gt; player.&lt;/p&gt;
&lt;p&gt;In &lt;a href="https://news.ycombinator.com/item?id=43803243#43804551"&gt;a thread&lt;/a&gt; about meta-knowledge of the StreetView card uses in different regions:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The photography matters a great deal - they're categorized into "Generations" of coverage. Gen 2 is low resolution, Gen 3 is pretty good but has a distinct car blur, Gen 4 is highest quality. Each country tends to have only one or two categories of coverage, and some are so distinct you can immediately know a location based solely on that (India is the best example here). [...]&lt;/p&gt;
&lt;p&gt;Nigeria and Tunisia have follow cars. Senegal, Montenegro and Albania have large rifts in the sky where the panorama stitching software did a poor job. Some parts of Russia had recent forest fires and are very smokey. One road in Turkey is in absurdly thick fog. The list is endless, which is why it's so fun!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sam also has &lt;a href="https://news.ycombinator.com/item?id=43803243#43804197"&gt;his own custom Obsidian flashcard deck&lt;/a&gt; "with hundreds of entries to help me remember road lines, power poles, bollards, architecture, license plates, etc".&lt;/p&gt;
&lt;p&gt;I &lt;a href="https://news.ycombinator.com/item?id=43805123"&gt;asked Sam&lt;/a&gt; how closely the GeoGuessr community track updates to street view imagery, and unsurprisingly those are a &lt;em&gt;big&lt;/em&gt; deal. Sam pointed me to &lt;a href="https://www.youtube.com/watch?v=XLETln6ZatE"&gt;this 10 minute video review&lt;/a&gt; by zi8gzag of the latest big update from three weeks ago:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is one of the biggest updates in years in my opinion. It could be the biggest update since the 2022 update that gave Gen 4 to Nigeria, Senegal, and Rwanda. It's definitely on the same level as the Kazakhstan update or the Germany update in my opinion.&lt;/p&gt;
&lt;/blockquote&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/streetview"&gt;streetview&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geoguessing"&gt;geoguessing&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="hacker-news"/><category term="streetview"/><category term="geoguessing"/></entry><entry><title>A5</title><link href="https://simonwillison.net/2025/Apr/22/a5/#atom-tag" rel="alternate"/><published>2025-04-22T14:37:36+00:00</published><updated>2025-04-22T14:37:36+00:00</updated><id>https://simonwillison.net/2025/Apr/22/a5/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://a5geo.org"&gt;A5&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A5 is a new "global, equal-area, millimeter-accurate geospatial index" by Felix Palmer:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It is the pentagonal equivalent of other DGGSs, like S2 or H3, but with higher accuracy and lower distortion.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Effectively it's a way of dividing the entire world into pentagons where each one covers the same physical area (to within a 2% threshold) - like Uber's &lt;a href="https://www.uber.com/blog/h3/"&gt;H3&lt;/a&gt; but a bit weirder and more fun. An A5 reference implementation written in TypeScript is &lt;a href="https://github.com/felixpalmer/a5"&gt;available on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This &lt;a href="https://a5geo.org/examples/cells"&gt;interactive demo&lt;/a&gt; helps show how it works:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Animated demo. I click around on a map, each click producing a distorted pentagon shape that tessellates with those nearby. The pentagons in Norway appear larger than those in Germany, which illustrates the distortion of the standard map projection." src="https://static.simonwillison.net/static/2025/a5.gif" /&gt;&lt;/p&gt;
&lt;p&gt;Why pentagons? Here's &lt;a href="https://a5geo.org/docs/"&gt;what the A5 docs say&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A5 is unique in that it uses a pentagonal tiling of a dodecahedron. [...] The benefit of choosing a dodecahedron is that it is the platonic solid with the lowest vertex curvature, and by this measure it is the most spherical of all the platonic solids. This is key for minimizing cell distortion as the process of projecting a platonic solid onto a sphere involves warping the cell geometry to force the vertex curvature to approach zero. Thus, the lower the original vertex curvature, the less distortion will be introduced by the projection.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I had to look up &lt;a href="https://en.wikipedia.org/wiki/Platonic_solid"&gt;platonic solids&lt;/a&gt; on Wikipedia. There are only five: Tetrahedron, Cube, Octahedron, Dodecahedron and Icosahedron and they can be made using squares, triangles or (in the case of the Dodecahedron) pentagons, making the pentagon the most circle-like option.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://bsky.app/profile/macwright.com/post/3ln6asbaduk2g"&gt;Tom MacWright&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/></entry><entry><title>OpenTimes</title><link href="https://simonwillison.net/2025/Mar/17/opentimes/#atom-tag" rel="alternate"/><published>2025-03-17T22:49:59+00:00</published><updated>2025-03-17T22:49:59+00:00</updated><id>https://simonwillison.net/2025/Mar/17/opentimes/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://sno.ws/opentimes/"&gt;OpenTimes&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Spectacular new open geospatial project by &lt;a href="https://sno.ws/"&gt;Dan Snow&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;OpenTimes is a database of pre-computed, point-to-point travel times between United States Census geographies. It lets you download bulk travel time data for free and with no limits.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://opentimes.org/?id=060816135022&amp;amp;mode=car#9.76/37.5566/-122.3085"&gt;what I get&lt;/a&gt; for travel times by car from El Granada, California:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Isochrone map showing driving times from the El Granada census tract to other places in the San Francisco Bay Area" src="https://static.simonwillison.net/static/2025/opentimes.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;The technical details are &lt;em&gt;fascinating&lt;/em&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The entire OpenTimes backend is just static Parquet files on &lt;a href="https://www.cloudflare.com/developer-platform/products/r2/"&gt;Cloudflare's R2&lt;/a&gt;. There's no RDBMS or running service, just files and a CDN. The whole thing costs about $10/month to host and costs nothing to serve. In my opinion, this is a &lt;em&gt;great&lt;/em&gt; way to serve infrequently updated, large public datasets at low cost (as long as you partition the files correctly).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sure enough, &lt;a href="https://developers.cloudflare.com/r2/pricing/"&gt;R2 pricing&lt;/a&gt; charges "based on the total volume of data stored" - $0.015 / GB-month for standard storage, then $0.36 / million requests for "Class B" operations which include reads. They charge nothing for outbound bandwidth.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;All travel times were calculated by pre-building the inputs (OSM, OSRM networks) and then distributing the compute over &lt;a href="https://github.com/dfsnow/opentimes/actions/workflows/calculate-times.yaml"&gt;hundreds of GitHub Actions jobs&lt;/a&gt;. This worked shockingly well for this specific workload (and was also completely free).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's a &lt;a href="https://github.com/dfsnow/opentimes/actions/runs/13094249792"&gt;GitHub Actions run&lt;/a&gt; of the &lt;a href="https://github.com/dfsnow/opentimes/blob/a6a5f7abcdd69559b3e29f360fe0ff0399dbb400/.github/workflows/calculate-times.yaml#L78-L80"&gt;calculate-times.yaml workflow&lt;/a&gt; which uses a matrix to run 255 jobs!&lt;/p&gt;
&lt;p&gt;&lt;img alt="GitHub Actions run: calculate-times.yaml run by workflow_dispatch taking 1h49m to execute 255 jobs with names like run-job (2020-01) " src="https://static.simonwillison.net/static/2025/opentimes-github-actions.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Relevant YAML:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  matrix:
    year: ${{ fromJSON(needs.setup-jobs.outputs.years) }}
    state: ${{ fromJSON(needs.setup-jobs.outputs.states) }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where those JSON files were created by the previous step, which reads in the year and state values from &lt;a href="https://github.com/dfsnow/opentimes/blob/a6a5f7abcdd69559b3e29f360fe0ff0399dbb400/data/params.yaml#L72-L132"&gt;this params.yaml file&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The query layer uses a single DuckDB database file with &lt;em&gt;views&lt;/em&gt; that point to static Parquet files via HTTP. This lets you query a table with hundreds of billions of records after downloading just the ~5MB pointer file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is a really creative use of DuckDB's feature that lets you run queries against large data from a laptop using HTTP range queries to avoid downloading the whole thing.&lt;/p&gt;
&lt;p&gt;The README shows &lt;a href="https://github.com/dfsnow/opentimes/blob/3439fa2c54af227e40997b4a5f55678739e0f6df/README.md#using-duckdb"&gt;how to use that from R and Python&lt;/a&gt; - I got this working in the &lt;code&gt;duckdb&lt;/code&gt; client (&lt;code&gt;brew install duckdb&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSTALL httpfs;
LOAD httpfs;
ATTACH 'https://data.opentimes.org/databases/0.0.1.duckdb' AS opentimes;

SELECT origin_id, destination_id, duration_sec
  FROM opentimes.public.times
  WHERE version = '0.0.1'
      AND mode = 'car'
      AND year = '2024'
      AND geography = 'tract'
      AND state = '17'
      AND origin_id LIKE '17031%' limit 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In answer to a question about adding public transit times &lt;a href="https://news.ycombinator.com/item?id=43392521#43393183"&gt;Dan said&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In the next year or so maybe. The biggest obstacles to adding public transit are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Collecting all the necessary scheduling data (e.g. GTFS feeds) for every transit system in the county. Not insurmountable since there are services that do this currently.&lt;/li&gt;
&lt;li&gt;Finding a routing engine that can compute nation-scale travel time matrices quickly. Currently, the two fastest open-source engines I've tried (OSRM and Valhalla) don't support public transit for matrix calculations and the engines that do support public transit (R5, OpenTripPlanner, etc.) are too slow.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://gtfs.org/"&gt;GTFS&lt;/a&gt; is a popular CSV-based format for sharing transit schedules - here's &lt;a href="https://gtfs.org/resources/data/"&gt;an official list&lt;/a&gt; of available feed directories.&lt;/p&gt;
&lt;p&gt;This whole project feels to me like a great example of the &lt;a href="https://simonwillison.net/2021/Jul/28/baked-data/"&gt;baked data&lt;/a&gt; architectural pattern in action.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=43392521"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/census"&gt;census&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-data"&gt;open-data&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openstreetmap"&gt;openstreetmap&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/parquet"&gt;parquet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/baked-data"&gt;baked-data&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/duckdb"&gt;duckdb&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http-range-requests"&gt;http-range-requests&lt;/a&gt;&lt;/p&gt;



</summary><category term="census"/><category term="geospatial"/><category term="open-data"/><category term="openstreetmap"/><category term="cloudflare"/><category term="parquet"/><category term="github-actions"/><category term="baked-data"/><category term="duckdb"/><category term="http-range-requests"/></entry><entry><title>wolf-h3-viewer.glitch.me</title><link href="https://simonwillison.net/2025/Mar/9/h3-viewer/#atom-tag" rel="alternate"/><published>2025-03-09T14:51:55+00:00</published><updated>2025-03-09T14:51:55+00:00</updated><id>https://simonwillison.net/2025/Mar/9/h3-viewer/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://wolf-h3-viewer.glitch.me/"&gt;wolf-h3-viewer.glitch.me&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Neat interactive visualization of Uber's &lt;a href="https://h3geo.org/"&gt;H3&lt;/a&gt; hexagonal geographical indexing mechanism.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Map showing H3 geospatial index hexagons overlaid on the Minneapolis-Saint Paul metropolitan area. Various H3 cell IDs are displayed including &amp;quot;852621b3fffffff&amp;quot;, &amp;quot;852621a7fffffff&amp;quot;, &amp;quot;8527526fffffff&amp;quot;, &amp;quot;85262cd3fffffff&amp;quot;, and &amp;quot;85262c83fffffff&amp;quot;. A sidebar shows input fields for &amp;quot;lat,lon&amp;quot; with a &amp;quot;Go&amp;quot; button and &amp;quot;valid H3 id&amp;quot; with a &amp;quot;Find&amp;quot; button. Text indicates &amp;quot;Current H3 resolution: 5&amp;quot; and &amp;quot;Tip: Clicking an H3 cell will copy its id to the clipboard.&amp;quot; Map attribution shows &amp;quot;Leaflet | © OpenStreetMap contributors&amp;quot;." src="https://static.simonwillison.net/static/2025/h3-map.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/clupasq/h3-viewer"&gt;the source code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Why does H3 use hexagons? Because &lt;a href="https://www.youtube.com/watch?v=thOifuHs6eY"&gt;Hexagons are the Bestagons&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When hexagons come together, they form three-sided joints 120 degrees apart. This, for the least material, is the most mechanically stable arrangement.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Only triangles, squares, and hexagons can tile a plane without gaps, and of those three shapes hexagons offer the best ratio of perimeter to area.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=43305920#43307944"&gt;Hacker News comment&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uber"&gt;uber&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="javascript"/><category term="leaflet"/><category term="uber"/></entry><entry><title>OpenStreetMap embed URL</title><link href="https://simonwillison.net/2024/Nov/25/openstreetmap-embed-url/#atom-tag" rel="alternate"/><published>2024-11-25T19:29:16+00:00</published><updated>2024-11-25T19:29:16+00:00</updated><id>https://simonwillison.net/2024/Nov/25/openstreetmap-embed-url/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.openstreetmap.org/export/embed.html?bbox=-122.61343002319336,37.43138681508927,-122.38220214843751,37.5594114838176&amp;amp;layer=mapnik&amp;amp;marker=37.4954206394371,-122.4979019165039"&gt;OpenStreetMap embed URL&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I just found out OpenStreetMap have a "share" button which produces HTML for an iframe targetting &lt;code&gt;https://www.openstreetmap.org/export/embed.html&lt;/code&gt;, making it easy to drop an OpenStreetMap map onto any web page that allows iframes.&lt;/p&gt;
&lt;p&gt;As far as I can tell the supported parameters are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bbox=&lt;/code&gt; then min longitude, min latitude, max longitude, max latitude&lt;/li&gt;
&lt;li&gt;&lt;code&gt;marker=&lt;/code&gt; optional latitude, longitude coordinate for a marker (only a single marker is supported)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;layer=mapnik&lt;/code&gt; - other values I've found that work are &lt;code&gt;cyclosm&lt;/code&gt;, &lt;code&gt;cyclemap&lt;/code&gt;, &lt;code&gt;transportmap&lt;/code&gt; and &lt;code&gt;hot&lt;/code&gt; (for humanitarian)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's HTML for embedding this on a page using a sandboxed iframe - the &lt;code&gt;allow-scripts&lt;/code&gt; is necessary for the map to display.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;iframe
  sandbox="allow-scripts"
  style="border: none; width: 100%; height: 20em;"
  src="https://www.openstreetmap.org/export/embed.html?bbox=-122.613%2C37.431%2C-122.382%2C37.559&amp;amp;amp;layer=mapnik&amp;amp;amp;marker=37.495%2C-122.497"
&amp;gt;&amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;iframe
      sandbox="allow-scripts"
      style="border: none; width: 100%; height: 20em;"
      src="https://www.openstreetmap.org/export/embed.html?bbox=-122.613%2C37.431%2C-122.382%2C37.559&amp;amp;layer=mapnik&amp;amp;marker=37.495%2C-122.497"
&gt;&lt;/iframe&gt;
&lt;/p&gt;
&lt;p&gt;Thanks to this post I learned that iframes are rendered correctly in &lt;a href="https://fedi.simonwillison.net/@simon/113545275313339806"&gt;NetNewsWire&lt;/a&gt;, &lt;a href="https://fosstodon.org/@carlton/113545449230432890"&gt;NewsExplorer&lt;/a&gt;, &lt;a href="https://mstdn.social/@nriley/113545545163094439"&gt;NewsBlur&lt;/a&gt; and &lt;a href="https://fosstodon.org/@omad/113545693553360791"&gt;Feedly on Android&lt;/a&gt;.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/iframes"&gt;iframes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openstreetmap"&gt;openstreetmap&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sandboxing"&gt;sandboxing&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="iframes"/><category term="openstreetmap"/><category term="sandboxing"/></entry><entry><title>Foursquare Open Source Places: A new foundational dataset for the geospatial community</title><link href="https://simonwillison.net/2024/Nov/20/foursquare-open-source-places/#atom-tag" rel="alternate"/><published>2024-11-20T05:52:38+00:00</published><updated>2024-11-20T05:52:38+00:00</updated><id>https://simonwillison.net/2024/Nov/20/foursquare-open-source-places/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://location.foursquare.com/resources/blog/products/foursquare-open-source-places-a-new-foundational-dataset-for-the-geospatial-community/"&gt;Foursquare Open Source Places: A new foundational dataset for the geospatial community&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I did not expect this!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[...] we are announcing today the general availability of a foundational open data set, Foursquare Open Source Places ("FSQ OS Places"). This base layer of 100mm+ global places of interest ("POI") includes 22 core attributes (see schema &lt;a href="https://docs.foursquare.com/data-products/docs/places-os-data-schema"&gt;here&lt;/a&gt;) that will be updated monthly and available for commercial use under the Apache 2.0 license framework.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The data is available &lt;a href="https://docs.foursquare.com/data-products/docs/access-fsq-os-places"&gt;as Parquet files&lt;/a&gt; hosted on Amazon S3.&lt;/p&gt;
&lt;p&gt;Here's how to list the available files:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aws s3 ls s3://fsq-os-places-us-east-1/release/dt=2024-11-19/places/parquet/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I got back &lt;code&gt;places-00000.snappy.parquet&lt;/code&gt; through &lt;code&gt;places-00024.snappy.parquet&lt;/code&gt;, each file around 455MB for a total of 10.6GB of data.&lt;/p&gt;
&lt;p&gt;I ran &lt;code&gt;duckdb&lt;/code&gt; and then used DuckDB's ability to remotely query Parquet on S3 to explore the data a bit more without downloading it to my laptop first:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select count(*) from 's3://fsq-os-places-us-east-1/release/dt=2024-11-19/places/parquet/places-00000.snappy.parquet';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This got back 4,180,424 - that number is similar for each file, suggesting around 104,000,000 records total.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; DuckDB can use wildcards in S3 paths (thanks, &lt;a href="https://mas.to/@paulbailey/113520325087085448"&gt;Paul&lt;/a&gt;) so this query provides an exact count:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select count(*) from 's3://fsq-os-places-us-east-1/release/dt=2024-11-19/places/parquet/places-*.snappy.parquet';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That returned 104,511,073 - and Activity Monitor on my Mac confirmed that DuckDB only needed to fetch 1.2MB of data to answer that query.&lt;/p&gt;
&lt;p&gt;I ran this query to retrieve 1,000 places from that first file as newline-delimited JSON:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;copy (
    select * from 's3://fsq-os-places-us-east-1/release/dt=2024-11-19/places/parquet/places-00000.snappy.parquet'
    limit 1000
) to '/tmp/places.json';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/53ad57ad42c7efe75e2028d975907180"&gt;that places.json file&lt;/a&gt;, and here it is &lt;a href="https://lite.datasette.io/?json=https://gist.github.com/simonw/53ad57ad42c7efe75e2028d975907180#/data/raw"&gt;imported into Datasette Lite&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, I got ChatGPT Code Interpreter to &lt;a href="https://chatgpt.com/share/673d7b92-0b4c-8006-a442-c5e6c2713d9c"&gt;convert that file to GeoJSON&lt;/a&gt; and pasted the result &lt;a href="https://gist.github.com/simonw/1e2a170b7368932ebd3922cb5d234924"&gt;into this Gist&lt;/a&gt;, giving me a map of those thousand places (because Gists automatically render GeoJSON):&lt;/p&gt;
&lt;p&gt;&lt;img alt="A map of the world with 1000 markers on it. A marker in Columbia shows a dialog for Raisbeck, Bogota Dv, Cra 47 A 114 05 Second Floor" src="https://static.simonwillison.net/static/2024/places-geojson.jpg" /&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://waxy.org/2024/11/foursquare-open-sources-its-places-database/"&gt;Andy Baio&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/foursquare"&gt;foursquare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geojson"&gt;geojson&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/parquet"&gt;parquet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/duckdb"&gt;duckdb&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-lite"&gt;datasette-lite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/code-interpreter"&gt;code-interpreter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="open-source"/><category term="foursquare"/><category term="geojson"/><category term="parquet"/><category term="duckdb"/><category term="datasette-lite"/><category term="ai-assisted-programming"/><category term="code-interpreter"/><category term="coding-agents"/></entry><entry><title>OpenStreetMap vector tiles demo</title><link href="https://simonwillison.net/2024/Nov/19/openstreetmap-vector-tiles-demo/#atom-tag" rel="alternate"/><published>2024-11-19T23:39:18+00:00</published><updated>2024-11-19T23:39:18+00:00</updated><id>https://simonwillison.net/2024/Nov/19/openstreetmap-vector-tiles-demo/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://pnorman.github.io/tilekiln-shortbread-demo/#9.23/37.5982/-122.2625"&gt;OpenStreetMap vector tiles demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Long-time OpenStreetMap developer &lt;a href="https://www.paulnorman.ca/"&gt;Paul Norman&lt;/a&gt; has been working on adding vector tile support to OpenStreetMap for &lt;a href="https://community.openstreetmap.org/t/minutely-updated-vector-tiles-demo/110121"&gt;quite a while&lt;/a&gt;. Paul &lt;a href="https://community.openstreetmap.org/t/vector-tiles-on-osmf-hardware/121501"&gt;recently announced&lt;/a&gt; that &lt;code&gt;vector.openstreetmap.org&lt;/code&gt; is now serving vector tiles (in &lt;a href="https://github.com/mapbox/vector-tile-spec"&gt;Mapbox Vector Tiles (MVT) format&lt;/a&gt;) - here's his interactive demo for seeing what they look like.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://tech.marksblogg.com/osm-mvt-vector-tiles.html"&gt;Mark Litwintschik&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mapping"&gt;mapping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openstreetmap"&gt;openstreetmap&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="mapping"/><category term="openstreetmap"/></entry><entry><title>Visualizing local election results with Datasette, Observable and MapLibre GL</title><link href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#atom-tag" rel="alternate"/><published>2024-11-09T23:32:06+00:00</published><updated>2024-11-09T23:32:06+00:00</updated><id>https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#atom-tag</id><summary type="html">
    &lt;p&gt;Alex Garcia and myself hosted the first &lt;a href="https://simonwillison.net/2024/Nov/7/datasette-public-office-hours/"&gt;Datasette Open Office Hours&lt;/a&gt; on Friday - a live-streamed video session where we hacked on a project together and took questions and tips from community members on Discord.&lt;/p&gt;
&lt;p&gt;We didn't record this one (surprisingly not a feature that Discord offers) but we hope to do more of these and record them in the future.&lt;/p&gt;
&lt;p&gt;This post is a detailed write-up of what we built during the session.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#san-mateo-county-election-results"&gt;San Mateo County election results&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#importing-csv-data-into-datasette"&gt;Importing CSV data into Datasette&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#modifying-the-schema"&gt;Modifying the schema&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#faceting-and-filtering-the-table"&gt;Faceting and filtering the table&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#importing-geospatial-precinct-shapes"&gt;Importing geospatial precinct shapes&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#enriching-that-data-to-extract-the-precinct-ids"&gt;Enriching that data to extract the precinct IDs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#running-a-join"&gt;Running a join&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#creating-an-api-token-to-access-the-data"&gt;Creating an API token to access the data&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#getting-cors-working"&gt;Getting CORS working&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#working-with-datasette-in-observable"&gt;Working with Datasette in Observable&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#visualizing-those-with-maplibre-gl"&gt;Visualizing those with MapLibre GL&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#observable-plot"&gt;Observable Plot&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#bringing-it-all-together"&gt;Bringing it all together&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#we-ll-be-doing-this-again"&gt;We'll be doing this again&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h4 id="san-mateo-county-election-results"&gt;San Mateo County election results&lt;/h4&gt;
&lt;p&gt;I live in El Granada, a tiny town just north of Half Moon Bay in San Mateo County, California.&lt;/p&gt;
&lt;p&gt;Every county appears to handle counting and publishing election results differently. For San Mateo County the results are published &lt;a href="https://smcacre.gov/elections/november-5-2024-election-results"&gt;on this page&lt;/a&gt;, and detailed per-precinct and per-candidate breakdowns are made available as a CSV file.&lt;/p&gt;
&lt;p&gt;(I optimistically set up a &lt;a href="https://simonwillison.net/2020/Oct/9/git-scraping/"&gt;Git scraper&lt;/a&gt; for these results in &lt;a href="https://github.com/simonw/scrape-san-mateo-county-election-results-2024"&gt;simonw/scrape-san-mateo-county-election-results-2024&lt;/a&gt; only to learn that the CSV is updated just once a day, not continually as the ballots are counted.)&lt;/p&gt;
&lt;p&gt;I'm particularly invested in the results of the &lt;a href="http://granada.ca.gov/"&gt;Granada Community Services District&lt;/a&gt; board member elections. Our little town of El Granada is in "unincorporated San Mateo County" which means we don't have a mayor or any local officials, so the closest we get to hyper-local government is the officials that run our local sewage and parks organization! My partner Natalie ran &lt;a href="https://til.simonwillison.net/youtube/livestreaming"&gt;the candidate forum event&lt;/a&gt; (effectively the debate) featuring three of the four candidates running for the two open places on the board.&lt;/p&gt;
&lt;p&gt;Let's explore the data for that race using Datasette.&lt;/p&gt;
&lt;h4 id="importing-csv-data-into-datasette"&gt;Importing CSV data into Datasette&lt;/h4&gt;
&lt;p&gt;I ran my part of the demo using &lt;a href="https://www.datasette.cloud/"&gt;Datasette Cloud&lt;/a&gt;, the beta of my new hosted Datasette service.&lt;/p&gt;
&lt;p&gt;I started by using the pre-configured &lt;a href="https://github.com/datasette/datasette-import"&gt;datasette-import&lt;/a&gt; plugin to import the data from the CSV file into a fresh table:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/datasette-import-loop.gif" alt="Paste data to create a table - I drag and drop on a CSV file, which produces a preview of the first 100 of 15,589 rows. I click to Upload and a progress bar runs before redirecting me to the resulting table." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="modifying-the-schema"&gt;Modifying the schema&lt;/h4&gt;
&lt;p&gt;The table imported cleanly, but all of the columns from the CSV were still being treated as text. I used the &lt;a href=""&gt;datasette-edit-schema&lt;/a&gt; plugin to switch the relevant columns to integers so that we could run sums and sorts against them.&lt;/p&gt;
&lt;p&gt;(I also noted that I really should add a "detect column types" feature to that plugin!)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/datasette-edit-schema.jpg" alt="Edit table data/san_mateo_election_results - an option to rename table and then one to change existing columns, where each column is listed in turn and some have their type select box set to integer instead of the default of text" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;The resulting 15,589 rows represent counts from individual precincts around the county for each of the races and measures on the ballot, with a row per precinct per candidate/choice per race.&lt;/p&gt;
&lt;h4 id="faceting-and-filtering-the-table"&gt;Faceting and filtering the table&lt;/h4&gt;
&lt;p&gt;Since I'm interested in the Granada Community Services District election, I applied a facet on "Contest_title" and then used that to select that specific race.&lt;/p&gt;
&lt;p&gt;I applied additional facets on "candidate_name" and "Precinct name".&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/facet-candidates.jpg" alt="28 rows where Contest_title = Granada Community Services District Members, Board of Directors. Facets are precinct name (7 choices), candidate name (IRIS GRANT, JANET BRAYER, NANCY MARSH, WANDA BOWLES) and Contest_title" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This looks right to me: we have 7 precincts and 4 candidates for 28 rows in total.&lt;/p&gt;
&lt;h4 id="importing-geospatial-precinct-shapes"&gt;Importing geospatial precinct shapes&lt;/h4&gt;
&lt;p&gt;Those precinct names are pretty non-descriptive! What does 33001 mean?&lt;/p&gt;
&lt;p&gt;To answer that question, I added a new table.&lt;/p&gt;
&lt;p&gt;San Mateo County offers &lt;a href="https://smcacre.gov/elections/precinct-maps-pdf"&gt;precinct maps&lt;/a&gt; in the form of 23 PDF files. Our precincts are in the "Unincorporated Coastside" file:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/precinct-map-from-pdf.jpg" alt="Screenshot from a PDF - label is Unincorporated Coastside, it shows the area north of Half Moon Bay with a bunch of polygons with numeric identifiers." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Thankfully the county &lt;em&gt;also&lt;/em&gt; makes that data available as &lt;a href="https://data.smcgov.org/Government/Election-Precincts/g5sj-6zp8/about_data"&gt;geospatial data&lt;/a&gt;, hosted using Socrata with an option to export as GeoJSON.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/precincts-socrata.jpg" alt="Socrata interface, Election Precincts updated March 7 2022 - 533 views, 72 downloads, and export dataset modal shows a GeoJSON option to export 783 rows." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;datasette-import&lt;/code&gt; plugin can handle JSON files... and if a JSON file contains a top-level object with a key that is an array of objects, it will import those objects as a table.&lt;/p&gt;
&lt;p&gt;Dragging that file into Datasette is enough to import it as a table with a &lt;code&gt;properties&lt;/code&gt; JSON column containing properties and a &lt;code&gt;geometry&lt;/code&gt; JSON columnn with the GeoJSON geometry.&lt;/p&gt;
&lt;p&gt;Here's where another plugin kicks in: &lt;a href="https://datasette.io/plugins/datasette-leaflet-geojson"&gt;datasette-leaflet-geojson&lt;/a&gt; looks for columns that contain valid GeoJSON geometries and... draws them on a map!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/precincts-in-datasette.jpg" alt="Datasette precincts table with 783 rows. The properties column contains JSON keys lastupdate, creationda, prencitid, notes and active - the geometry column renders maps with polygons showing the shape of the precinct." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;So now we can see the shape of the individual geometries.&lt;/p&gt;
&lt;h4 id="enriching-that-data-to-extract-the-precinct-ids"&gt;Enriching that data to extract the precinct IDs&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;precinctid&lt;/code&gt; is present in the data, but it's tucked away in a JSON object in that &lt;code&gt;properties&lt;/code&gt; JSON blob. It would be more convenient if it was a top-level column.&lt;/p&gt;
&lt;p&gt;Datasette's &lt;a href="https://simonwillison.net/2023/Dec/1/datasette-enrichments/"&gt;enrichments feature&lt;/a&gt; provides tools for running operations against every row in a table and adding new columns based on the results.&lt;/p&gt;
&lt;p&gt;My Datasette Cloud instance was missing the &lt;a href="https://github.com/datasette/datasette-enrichments-quickjs"&gt;datasette-enrichments-quickjs plugin&lt;/a&gt; that would let me run JavaScript code against the data. I used my privileged access on Datasette Cloud to add that plugin to my requirements and restarted the instance to install it.&lt;/p&gt;
&lt;p&gt;I used that to run this JavaScript code against every row in the table and saved the output in a new &lt;code&gt;precinct_id&lt;/code&gt; column:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;enrich&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;row&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;parse&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;row&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;properties&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;precinctid&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/enrich-precincts.jpg" alt="Enrich data in precincts. 783 rows selected. JavaScript. Enrich data with a custom JavaScript function. JavaScript function: function enrich(row) { return JSON.stringify(row) + &amp;quot; enriched&amp;quot;; } - Define an enrich(row) JavaScript function taking an object and returning a value. Row keys: properties, geometry. Output mode: store the function result in a single column. Output clumn name: precinct_id. The column to store the output in - will be created if it does not exist. Output column type: text." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This took less than a second to run, adding and populating a new &lt;code&gt;precinct_id&lt;/code&gt; column for the table.&lt;/p&gt;
&lt;h4 id="running-a-join"&gt;Running a join&lt;/h4&gt;
&lt;p&gt;I demonstrated how to run a join between the election results and the precincts table using the Datasette SQL query editor.&lt;/p&gt;
&lt;p&gt;I tried a few different things, but the most interesting query was this one:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt;
  Precinct_name,
  &lt;span class="pl-c1"&gt;precincts&lt;/span&gt;.&lt;span class="pl-c1"&gt;geometry&lt;/span&gt;,
  total_ballots,
  json_group_object(
    candidate_name,
    total_votes
  ) &lt;span class="pl-k"&gt;as&lt;/span&gt; votes_by_candidate
&lt;span class="pl-k"&gt;from&lt;/span&gt;
  election_results 
  &lt;span class="pl-k"&gt;join&lt;/span&gt; precincts &lt;span class="pl-k"&gt;on&lt;/span&gt; &lt;span class="pl-c1"&gt;election_results&lt;/span&gt;.&lt;span class="pl-c1"&gt;Precinct_name&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;precincts&lt;/span&gt;.&lt;span class="pl-c1"&gt;precinct_id&lt;/span&gt;
&lt;span class="pl-k"&gt;where&lt;/span&gt; 
  Contest_title &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Granada Community Services District Members, Board of Directors&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;group by&lt;/span&gt; 
  Precinct_name,
  &lt;span class="pl-c1"&gt;precincts&lt;/span&gt;.&lt;span class="pl-c1"&gt;geometry&lt;/span&gt;,
  total_ballots;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/joined-precincts.jpg" alt="The SQL query returned four columns: Precinct_name, geometry with a map of the precinct, total_ballots with a number and votes_by_candidate with a JSON object mapping each candidate name to their number of votes." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="creating-an-api-token-to-access-the-data"&gt;Creating an API token to access the data&lt;/h4&gt;
&lt;p&gt;I was nearly ready to hand over to Alex for the second half of our demo, where he would use Observable Notebooks to build some custom visualizations on top of the data.&lt;/p&gt;
&lt;p&gt;A great pattern for this is to host the data in Datasette and then fetch it into Observable via the Datasette JSON API.&lt;/p&gt;
&lt;p&gt;Since Datasette Cloud instances are private by default we would need to create an API token that could do this.&lt;/p&gt;
&lt;p&gt;I used this interface (from the &lt;a href="https://github.com/simonw/datasette-auth-tokens"&gt;datasette-auth-tokens plugin&lt;/a&gt;) to create a new token with read-only access to all databases and tables in the instance:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/create-api-token.jpg" alt="Create an API token interface. This token will allow API access with the same abilities as your current user, swillison .Token will be restricted to: all databases and tables: view-database, all databases and tables: view-table, all databases and tables: execute-sql - token is set to read-only and never expires, a list of possible permissions with checkboxes is listed below." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Since we're running a dedicated instance just for Datasette Public Office Hours there's no reason not to distribute that read-only token in publically accessible code.&lt;/p&gt;
&lt;h4 id="getting-cors-working"&gt;Getting CORS working&lt;/h4&gt;
&lt;p&gt;Embarrassingly, I had forgotten that we would need CORS headers in order to access the data from an Observable notebook. Thankfully we have another plugin for that: &lt;a href="https://datasette.io/plugins/datasette-cors"&gt;datasette-cors&lt;/a&gt;. I installed that quickly and we confirmed that it granted access to the API from Observable as intended.&lt;/p&gt;
&lt;p&gt;I handed over to Alex for the next section of the demo.&lt;/p&gt;
&lt;h4 id="working-with-datasette-in-observable"&gt;Working with Datasette in Observable&lt;/h4&gt;
&lt;p&gt;Alex started by running a SQL query from client-side JavaScript to pull in the joined data for our specific El Granada race:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;sql&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;`&lt;/span&gt;
&lt;span class="pl-s"&gt;select&lt;/span&gt;
&lt;span class="pl-s"&gt;  Precinct_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  precincts.geometry,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Split_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Reporting_flag,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Update_count,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Pct_Id,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Pct_seq_nbr,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Reg_voters,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Turn_Out,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Contest_Id,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Contest_seq_nbr,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Contest_title,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Contest_party_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Selectable_Options,&lt;/span&gt;
&lt;span class="pl-s"&gt;  candidate_id,&lt;/span&gt;
&lt;span class="pl-s"&gt;  candidate_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Candidate_Type,&lt;/span&gt;
&lt;span class="pl-s"&gt;  cand_seq_nbr,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Party_Code,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_ballots,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_votes,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_under_votes,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_over_votes,&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote Centers_ballots],&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote Centers_votes],&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote Centers_under_votes],&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote Centers_over_votes],&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote by Mail_ballots],&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote by Mail_votes],&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote by Mail_under_votes],&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote by Mail_over_votes]&lt;/span&gt;
&lt;span class="pl-s"&gt;from&lt;/span&gt;
&lt;span class="pl-s"&gt;  election_results join precincts on election_results.Precinct_name = precincts.precinct_id&lt;/span&gt;
&lt;span class="pl-s"&gt;where "Contest_title" = "Granada Community Services District Members, Board of Directors"&lt;/span&gt;
&lt;span class="pl-s"&gt;limit 101;`&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And in the next cell:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;raw_data&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-s"&gt;`https://datasette-public-office-hours.datasette.cloud/data/-/query.json?_shape=array&amp;amp;sql=&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-en"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;    &lt;span class="pl-s1"&gt;sql&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;  &lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;`&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;headers&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;Authorization&lt;/span&gt;: &lt;span class="pl-s"&gt;`Bearer &lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;secret&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;`&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;r&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;r&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note the &lt;code&gt;?_shape=array&lt;/code&gt; parameter there, which causes Datasette to output the results directly as a JSON array of objects.&lt;/p&gt;
&lt;p&gt;That's all it takes to get the data into Observable. Adding another cell like this confirms that the data is now available:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-v"&gt;Inputs&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;table&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;raw_data&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/inputs-table-raw-data.jpg" alt="An Observable cell running Inputs.table(raw_data) and displaying a table of Precinct_name and geometry columns, with GeoJSON" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="visualizing-those-with-maplibre-gl"&gt;Visualizing those with MapLibre GL&lt;/h4&gt;
&lt;p&gt;There are plenty of good options for visualizing GeoJSON data using JavaScript in an Observable notebook.&lt;/p&gt;
&lt;p&gt;Alex started with &lt;a href="https://maplibre.org/maplibre-gl-js/docs/"&gt;MapLibre GL&lt;/a&gt;, using the excellent &lt;a href="https://simonwillison.net/2024/Sep/28/openfreemap/"&gt;OpenFreeMap 3D tiles&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;viewof&lt;/span&gt; &lt;span class="pl-s1"&gt;map&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-s1"&gt;const&lt;/span&gt; container &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;html&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;div&lt;/span&gt; &lt;span class="pl-c1"&gt;style&lt;/span&gt;="&lt;span class="pl-s"&gt;height:800px;&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;`&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;yield&lt;/span&gt; &lt;span class="pl-s1"&gt;container&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;map&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;container&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;value&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-s1"&gt;maplibregl&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;Map&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
    container&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;zoom&lt;/span&gt;: &lt;span class="pl-c1"&gt;2&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c"&gt;//style: "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json",&lt;/span&gt;
    &lt;span class="pl-c1"&gt;style&lt;/span&gt;: &lt;span class="pl-s"&gt;"https://tiles.openfreemap.org/styles/liberty"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;scrollZoom&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;yield&lt;/span&gt; &lt;span class="pl-s1"&gt;container&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

  &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;on&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"load"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;fitBounds&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d3&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;geoBounds&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;duration&lt;/span&gt;: &lt;span class="pl-c1"&gt;0&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addSource&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"precincts"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"geojson"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;data&lt;/span&gt;: &lt;span class="pl-s1"&gt;data&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addLayer&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;id&lt;/span&gt;: &lt;span class="pl-s"&gt;"precincts"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"fill"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;source&lt;/span&gt;: &lt;span class="pl-s"&gt;"precincts"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;paint&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
        &lt;span class="pl-s"&gt;"fill-opacity"&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.4&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
        &lt;span class="pl-s"&gt;"fill-color"&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;
          &lt;span class="pl-s"&gt;"case"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;"=="&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;"get"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"ratio"&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;null&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"#000000"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-kos"&gt;[&lt;/span&gt;
            &lt;span class="pl-s"&gt;"interpolate"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;"linear"&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;"get"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"ratio"&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;0.0&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"#0000ff"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;0.5&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"#d3d3d3"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;1.0&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"#ff0000"&lt;/span&gt;
          &lt;span class="pl-kos"&gt;]&lt;/span&gt;
        &lt;span class="pl-kos"&gt;]&lt;/span&gt;
      &lt;span class="pl-kos"&gt;}&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;on&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"click"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"precincts"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;e&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; precinct&lt;span class="pl-kos"&gt;,&lt;/span&gt; ratio &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;e&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;features&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;properties&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;description&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;stringify&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

      &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-s1"&gt;maplibregl&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;Popup&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
        &lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;setLngLat&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;e&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;lngLat&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
        &lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;setHTML&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;description&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
        &lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addTo&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-s1"&gt;invalidation&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;remove&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/maplibre-gl.jpg" alt="An Observable cell showing a map of El Granada - a black shape shows the outlines of the precincts." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;(This is just one of several iterations, I didn't capture detailed notes of every change Alex made to the code.)&lt;/p&gt;
&lt;h4 id="observable-plot"&gt;Observable Plot&lt;/h4&gt;
&lt;p&gt;Observable notebooks come pre-loaded with the excellent Observable Plot charting library - Mike Bostock's high-level charting tool built on top of D3.&lt;/p&gt;
&lt;p&gt;Alex used that to first render the shapes of the precincts directly, without even needing a tiled basemap:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-v"&gt;Plot&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;plot&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
  width&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;height&lt;/span&gt;: &lt;span class="pl-c1"&gt;600&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;legend&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;projection&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"conic-conformal"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;parallels&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;37&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-c1"&gt;4&lt;/span&gt; &lt;span class="pl-c1"&gt;/&lt;/span&gt; &lt;span class="pl-c1"&gt;60&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;38&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-c1"&gt;26&lt;/span&gt; &lt;span class="pl-c1"&gt;/&lt;/span&gt; &lt;span class="pl-c1"&gt;60&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;rotate&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;120&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-c1"&gt;30&lt;/span&gt; &lt;span class="pl-c1"&gt;/&lt;/span&gt; &lt;span class="pl-c1"&gt;60&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;domain&lt;/span&gt;: &lt;span class="pl-s1"&gt;data&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;marks&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;
    &lt;span class="pl-v"&gt;Plot&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;geo&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;strokeOpacity&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.1&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;fill&lt;/span&gt;: &lt;span class="pl-s"&gt;"total_votes"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-en"&gt;title&lt;/span&gt;: &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;stringify&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;properties&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;tip&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
  &lt;span class="pl-kos"&gt;]&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;parallels&lt;/code&gt; and &lt;code&gt;rotate&lt;/code&gt; options there come from the handy &lt;a href="https://github.com/veltman/d3-stateplane?tab=readme-ov-file#nad83--california-zone-3-epsg26943"&gt;veltman/d3-stateplane&lt;/a&gt; repo, which lists recommended settings for the &lt;a href="https://en.wikipedia.org/wiki/State_Plane_Coordinate_System"&gt;State Plane Coordinate System&lt;/a&gt; used with projections in D3. Those values are for &lt;a href="https://www.conservation.ca.gov/cgs/rgm/state-plane-coordinate-system"&gt;California Zone 3&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/color-precincts.jpg" alt="An Observable cell shows six five distinct colored polygons, each for a different precinct. The shape of El Granada is clearly visible despite no other map tiles or labels." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="bringing-it-all-together"&gt;Bringing it all together&lt;/h4&gt;
&lt;p&gt;For the grand finale, Alex combined everything learned so far to build an interactive map allowing a user to select any of the 110 races on the ballot and see a heatmap of results for any selected candidate and option:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/select-map.gif" alt="Animated demo. Choose a contest select - picking different contests updates the map at the bottom. For each contest the candidates or options are shown as radio buttons, and selecting those updates the map to show a heatmap of votes for that candidate in different precincts." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;You can try this out in &lt;a href="https://observablehq.com/d/2ed2ad2443d7bbb5"&gt;Alex's notebook&lt;/a&gt;. Here's the relevant code (Observable cells are divided by &lt;code&gt;// ---&lt;/code&gt; comments). Note that Observable notebooks are reactive and allow variables to be referenced out of order.&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-c"&gt;// Select the contest&lt;/span&gt;
&lt;span class="pl-s1"&gt;viewof&lt;/span&gt; &lt;span class="pl-s1"&gt;contest&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Inputs&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;select&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;contests&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;label&lt;/span&gt;: &lt;span class="pl-s"&gt;"Choose a contest"&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;

&lt;span class="pl-c"&gt;// ---&lt;/span&gt;

&lt;span class="pl-c"&gt;// And the candidate&lt;/span&gt;
&lt;span class="pl-s1"&gt;viewof&lt;/span&gt;&lt;span class="pl-kos"&gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;candidate&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Inputs&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;radio&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-s1"&gt;candidates&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;label&lt;/span&gt;: &lt;span class="pl-s"&gt;"Choose a candidate"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;value&lt;/span&gt;: &lt;span class="pl-s1"&gt;candidates&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;

&lt;span class="pl-c"&gt;// ---&lt;/span&gt;

&lt;span class="pl-c"&gt;// Show the map itself&lt;/span&gt;
&lt;span class="pl-v"&gt;Plot&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;plot&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
  width&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;height&lt;/span&gt;: &lt;span class="pl-c1"&gt;600&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;legend&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;color&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;scheme&lt;/span&gt;: &lt;span class="pl-s"&gt;"blues"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;legend&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;projection&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"mercator"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;domain&lt;/span&gt;: &lt;span class="pl-s1"&gt;data2&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;marks&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;
    &lt;span class="pl-v"&gt;Plot&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;geo&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;data2&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;strokeOpacity&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.1&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;fill&lt;/span&gt;: &lt;span class="pl-s"&gt;"ratio"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;tip&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
  &lt;span class="pl-kos"&gt;]&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
# &lt;span class="pl-c1"&gt;--&lt;/span&gt;&lt;span class="pl-c1"&gt;-&lt;/span&gt;
&lt;span class="pl-s1"&gt;data2&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"FeatureCollection"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;features&lt;/span&gt;: &lt;span class="pl-s1"&gt;raw_data2&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"Feature"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;properties&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;precinct&lt;/span&gt;: &lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;Precinct_name&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;total_ballots&lt;/span&gt;: &lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;total_ballots&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;ratio&lt;/span&gt;: &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;parse&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;votes_by_candidate&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s1"&gt;candidate&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt; &lt;span class="pl-c1"&gt;/&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;total_ballots&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;geometry&lt;/span&gt;: &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;parse&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;geometry&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;

&lt;span class="pl-c"&gt;// ---&lt;/span&gt;

&lt;span class="pl-s1"&gt;raw_data2&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;query&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-s"&gt;`select&lt;/span&gt;
&lt;span class="pl-s"&gt;  Precinct_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  precincts.geometry,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_ballots,&lt;/span&gt;
&lt;span class="pl-s"&gt;  json_grop_object(&lt;/span&gt;
&lt;span class="pl-s"&gt;    candidate_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;    total_votes&lt;/span&gt;
&lt;span class="pl-s"&gt;  ) as votes_by_candidate&lt;/span&gt;
&lt;span class="pl-s"&gt;from&lt;/span&gt;
&lt;span class="pl-s"&gt;  election_results &lt;/span&gt;
&lt;span class="pl-s"&gt;  join precincts on election_results.Precinct_name = precincts.precinct_id&lt;/span&gt;
&lt;span class="pl-s"&gt;where Contest_title = :contest&lt;/span&gt;
&lt;span class="pl-s"&gt;group by &lt;/span&gt;
&lt;span class="pl-s"&gt;  Precinct_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  precincts.geometry,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_ballots;`&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;{&lt;/span&gt; contest &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;

&lt;span class="pl-c"&gt;// ---&lt;/span&gt;

&lt;span class="pl-s1"&gt;raw_data2&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;query&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-s"&gt;`select&lt;/span&gt;
&lt;span class="pl-s"&gt;  Precinct_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  precincts.geometry,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_ballots,&lt;/span&gt;
&lt;span class="pl-s"&gt;  json_group_object(&lt;/span&gt;
&lt;span class="pl-s"&gt;    candidate_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;    total_votes&lt;/span&gt;
&lt;span class="pl-s"&gt;  ) as votes_by_candidate&lt;/span&gt;
&lt;span class="pl-s"&gt;from&lt;/span&gt;
&lt;span class="pl-s"&gt;  election_results &lt;/span&gt;
&lt;span class="pl-s"&gt;  join precincts on election_results.Precinct_name = precincts.precinct_id&lt;/span&gt;
&lt;span class="pl-s"&gt;where Contest_title = :contest&lt;/span&gt;
&lt;span class="pl-s"&gt;group by &lt;/span&gt;
&lt;span class="pl-s"&gt;  Precinct_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  precincts.geometry,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_ballots;`&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;{&lt;/span&gt; contest &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;

&lt;span class="pl-c"&gt;// ---&lt;/span&gt;

&lt;span class="pl-c"&gt;// Fetch the available contests&lt;/span&gt;
&lt;span class="pl-s1"&gt;contests&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;query&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"select distinct Contest_title from election_results"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;Contest_title&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;

&lt;span class="pl-c"&gt;// ---&lt;/span&gt;

&lt;span class="pl-c"&gt;// Extract available candidates for selected contest&lt;/span&gt;

&lt;span class="pl-s1"&gt;candidates&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Object&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;keys&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;parse&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;raw_data2&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;votes_by_candidate&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;

&lt;span class="pl-c"&gt;// ---&lt;/span&gt;

&lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;query&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;sql&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;params&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-en"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
    &lt;span class="pl-s"&gt;`https://datasette-public-office-hours.datasette.cloud/data/-/query.json?&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;URLSearchParams&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;      &lt;span class="pl-kos"&gt;{&lt;/span&gt; sql&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;_shape&lt;/span&gt;: &lt;span class="pl-s"&gt;"array"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; ...&lt;span class="pl-s1"&gt;params&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;    &lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;toString&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;`&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;headers&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
        &lt;span class="pl-c1"&gt;Authorization&lt;/span&gt;: &lt;span class="pl-s"&gt;`Bearer &lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;secret&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;`&lt;/span&gt;
      &lt;span class="pl-kos"&gt;}&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;r&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;r&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id="we-ll-be-doing-this-again"&gt;We'll be doing this again&lt;/h4&gt;
&lt;p&gt;This was our first time trying something like this and I think it worked &lt;em&gt;really&lt;/em&gt; well. We're already thinking about ways to improve it next time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I want to record these sessions and make them available on YouTube for people who couldn't be there live&lt;/li&gt;
&lt;li&gt;It would be fun to mix up the format. I'm particularly keen on getting more people involved giving demos - maybe having 5-10 minute lightning demo slots so we can see what other people are working on&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keep an eye on this blog or on the &lt;a href="https://datasette.io/discord"&gt;Datasette Discord&lt;/a&gt; for news about future sessions.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mapping"&gt;mapping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/politics"&gt;politics&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-cloud"&gt;datasette-cloud&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/alex-garcia"&gt;alex-garcia&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-public-office-hours"&gt;datasette-public-office-hours&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="geospatial"/><category term="mapping"/><category term="politics"/><category term="projects"/><category term="datasette"/><category term="datasette-cloud"/><category term="alex-garcia"/><category term="datasette-public-office-hours"/><category term="leaflet"/></entry><entry><title>Conflating Overture Places Using DuckDB, Ollama, Embeddings, and More</title><link href="https://simonwillison.net/2024/Sep/30/conflating-overture-places/#atom-tag" rel="alternate"/><published>2024-09-30T17:24:03+00:00</published><updated>2024-09-30T17:24:03+00:00</updated><id>https://simonwillison.net/2024/Sep/30/conflating-overture-places/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.dbreunig.com/2024/09/27/conflating-overture-points-of-interests-with-duckdb-ollama-and-more.html"&gt;Conflating Overture Places Using DuckDB, Ollama, Embeddings, and More&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Drew Breunig's detailed tutorial on "conflation" - combining different geospatial data sources by de-duplicating address strings such as &lt;code&gt;RESTAURANT LOS ARCOS,3359 FOOTHILL BLVD,OAKLAND,94601&lt;/code&gt; and &lt;code&gt;LOS ARCOS TAQUERIA,3359 FOOTHILL BLVD,OAKLAND,94601&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Drew uses an entirely offline stack based around Python, DuckDB and Ollama and finds that a combination of H3 geospatial tiles and &lt;code&gt;mxbai-embed-large&lt;/code&gt; embeddings (though other embedding models should work equally well) gets really good results.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/duckdb"&gt;duckdb&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/embeddings"&gt;embeddings&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/drew-breunig"&gt;drew-breunig&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/overture"&gt;overture&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ollama"&gt;ollama&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="python"/><category term="ai"/><category term="duckdb"/><category term="embeddings"/><category term="drew-breunig"/><category term="overture"/><category term="ollama"/></entry><entry><title>OpenFreeMap</title><link href="https://simonwillison.net/2024/Sep/28/openfreemap/#atom-tag" rel="alternate"/><published>2024-09-28T21:41:15+00:00</published><updated>2024-09-28T21:41:15+00:00</updated><id>https://simonwillison.net/2024/Sep/28/openfreemap/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://openfreemap.org/"&gt;OpenFreeMap&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New free map tile hosting service from Zsolt Ero:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;OpenFreeMap lets you display custom maps on your website and apps for free. […] Using our &lt;strong&gt;public instance&lt;/strong&gt; is completely free: there are no limits on the number of map views or requests. There’s no registration, no user database, no API keys, and no cookies. We aim to cover the running costs of our public instance through donations.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The site serves static vector tiles that work with &lt;a href="https://maplibre.org/maplibre-gl-js/docs/"&gt;MapLibre GL&lt;/a&gt;. It deliberately doesn’t offer any other services such as search or routing.&lt;/p&gt;
&lt;p&gt;From &lt;a href="https://github.com/hyperknot/openfreemap"&gt;the project README&lt;/a&gt; looks like it’s hosted on two Hetzner machines. I don’t think the public server is behind a CDN.&lt;/p&gt;
&lt;p&gt;Part of the trick to serving the tiles efficiently is the way it takes advantage of &lt;a href="https://en.m.wikipedia.org/wiki/Btrfs"&gt;Btrfs&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Production-quality hosting of 300 million tiny files is hard. The average file size is just 450 byte. Dozens of tile servers have been written to tackle this problem, but they all have their limitations.&lt;/p&gt;
&lt;p&gt;The original idea of this project is to avoid using tile servers altogether. Instead, the tiles are directly served from Btrfs partition images + hard links using an optimised nginx config.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;a href="https://github.com/hyperknot/openfreemap/blob/main/docs/self_hosting.md"&gt;self-hosting guide&lt;/a&gt; describes the scripts that are provided for downloading their pre-built tiles (needing a fresh Ubuntu server with 300GB of SSD and 4GB of RAM) or building the tiles yourself using &lt;a href="https://github.com/onthegomap/planetiler"&gt;Planetiler&lt;/a&gt; (needs 500GB of disk and 64GB of RAM).&lt;/p&gt;
&lt;p&gt;Getting started is delightfully straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const map = new maplibregl.Map({
  style: 'https://tiles.openfreemap.org/styles/liberty',
  center: [13.388, 52.517],
  zoom: 9.5,
  container: 'map',
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I &lt;a href="https://gist.github.com/simonw/da2b20711b96f745873ccb44a3347ce9"&gt;got Claude to help&lt;/a&gt; build &lt;a href="http://tools.simonwillison.net/openfreemap-demo"&gt;this demo&lt;/a&gt; showing a thousand random markers dotted around San Francisco. The 3D tiles even include building shapes!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Map of San Francisco in 3D with building shapes and small blue random markers dotted around." src="https://static.simonwillison.net/static/2024/openfreemap.jpeg" /&gt;&lt;/p&gt;
&lt;p&gt;Zsolt built OpenFreeMap based on his experience running &lt;a href="https://maphub.net"&gt;MapHub&lt;/a&gt; over the last 9 years. Here’s &lt;a href="https://blog.opencagedata.com/post/interview-zsolt-ero-maphub"&gt;a 2018 interview about that project&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It’s pretty incredible that the OpenStreetMap and open geospatial stack has evolved to the point now where it’s economically feasible for an individual to offer a service like this. I hope this turns out to be sustainable. Hetzner charge &lt;a href="https://docs.hetzner.com/robot/general/traffic/"&gt;just €1 per TB&lt;/a&gt; for bandwidth (S3 can cost $90/TB) which should help a lot.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://cosocial.ca/@timbray/113216132761896850"&gt;Tim Bray&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/maps"&gt;maps&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openstreetmap"&gt;openstreetmap&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="maps"/><category term="openstreetmap"/><category term="ai-assisted-programming"/></entry><entry><title>Towards Standardizing Place</title><link href="https://simonwillison.net/2024/Aug/1/towards-standardizing-place/#atom-tag" rel="alternate"/><published>2024-08-01T23:14:49+00:00</published><updated>2024-08-01T23:14:49+00:00</updated><id>https://simonwillison.net/2024/Aug/1/towards-standardizing-place/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.dbreunig.com/2024/07/31/towards-standardizing-place.html"&gt;Towards Standardizing Place&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Overture Maps &lt;a href="https://overturemaps.org/overture-maps-foundation-releases-general-availability-of-its-open-maps-datasets/"&gt;announced General Availability of its global maps datasets&lt;/a&gt; last week, covering places, buildings, divisions, and base layers.&lt;/p&gt;
&lt;p&gt;Drew Breunig demonstrates how this can be accessed using both the &lt;a href="https://explore.overturemaps.org/#13.1/37.46975/-122.44309"&gt;Overture Explorer tool&lt;/a&gt; and DuckDB, and talks about Overture's GERS IDs - reminiscent of &lt;a href="https://whosonfirst.org/"&gt;Who's On First&lt;/a&gt; IDs - which provide stable IDs for all kinds of geographic places.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/whosonfirst"&gt;whosonfirst&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/drew-breunig"&gt;drew-breunig&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/overture"&gt;overture&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="whosonfirst"/><category term="drew-breunig"/><category term="overture"/></entry><entry><title>The many lives of Null Island</title><link href="https://simonwillison.net/2024/Jul/28/the-many-lives-of-null-island/#atom-tag" rel="alternate"/><published>2024-07-28T17:44:54+00:00</published><updated>2024-07-28T17:44:54+00:00</updated><id>https://simonwillison.net/2024/Jul/28/the-many-lives-of-null-island/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://stamen.com/the-many-lives-of-null-island/"&gt;The many lives of Null Island&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Stamen's custom basemaps have long harbored an Easter egg: zoom all the way in on 0, 0 to see the outline of the mystical "null island", the place where GIS glitches and data bugs accumulate, in the Gulf of Guinea south of Ghana.&lt;/p&gt;
&lt;p&gt;Stamen's Alan McConchie provides a detailed history of the Easter egg - first introduced by Mike Migurski in 2010 - along with a definitive guide to the GIS jokes and traditions that surround it.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://maps.stamen.com/toner/#19/0/0"&gt;Null Island on Stamen's Toner map&lt;/a&gt;. The shape (also available &lt;a href="https://github.com/stamen/toner-carto/blob/master/shp-local/nullisland.geojson"&gt;as GeoJSON&lt;/a&gt;) is an homage to the island from 1993's &lt;a href="https://en.wikipedia.org/wiki/Myst"&gt;Myst&lt;/a&gt;, hence the outline of a large docked ship at the bottom.&lt;/p&gt;
&lt;p&gt;&lt;img alt="White outline of Null Island on a black background." src="https://static.simonwillison.net/static/2024/null-island-toner.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Alan recently gave a talk about Stamen's updated custom maps at State of the Map US 2024 (&lt;a href="https://www.youtube.com/watch?v=qewW7-z8W2I"&gt;video&lt;/a&gt;, &lt;a href="https://speakerdeck.com/almccon/rebuilding-stamens-iconic-map-styles-with-stadia-maps"&gt;slides&lt;/a&gt;) - their Toner and Terrain maps are now available as vector tiles served by Stadia Maps (here's &lt;a href="https://stamen.com/stamen-x-stadia-the-end-of-the-road-for-stamens-legacy-map-tiles/"&gt;the announcement&lt;/a&gt;), but their iconic watercolor style is yet to be updated to vectors, due to the weird array of raster tricks it used to achieve the effect.&lt;/p&gt;
&lt;p&gt;In researching this post I searched for &lt;a href="https://www.google.com/maps/search/null+island/@6.3431564,-0.774215,5.21z"&gt;null island on Google Maps&lt;/a&gt; and was delighted to learn that a bunch of entrepreneurs in Western Africa have tapped into the meme for their own businesses:&lt;/p&gt;
&lt;p&gt;&lt;img alt="A null island search returns companies in The Gambia, Côte d’Ivoire, Burkina Faso, Cameroon and Democratic Republic of the Congo." src="https://static.simonwillison.net/static/2024/null-island-google-maps.jpg" /&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://waxy.org/2024/07/the-many-lives-of-null-island/"&gt;Andy Baio&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/maps"&gt;maps&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/michal-migurski"&gt;michal-migurski&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/stamen-design"&gt;stamen-design&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="maps"/><category term="michal-migurski"/><category term="stamen-design"/></entry><entry><title>Searching an aerial photo with text queries</title><link href="https://simonwillison.net/2024/Jul/12/searching-an-aerial-photo/#atom-tag" rel="alternate"/><published>2024-07-12T18:07:48+00:00</published><updated>2024-07-12T18:07:48+00:00</updated><id>https://simonwillison.net/2024/Jul/12/searching-an-aerial-photo/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.rtwilson.com/searching-an-aerial-photo-with-text-queries-a-demo-and-how-it-works/"&gt;Searching an aerial photo with text queries&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Robin Wilson built &lt;a href="https://server1.rtwilson.com/aerial/static/index.html"&gt;a demo&lt;/a&gt; that lets you search a large aerial photograph of Southampton for things like "roundabout" or "tennis court". He explains how it works in detail: he used the &lt;a href="https://github.com/wangzhecheng/SkyScript"&gt;SkyCLIP&lt;/a&gt; model, which is trained on "5.2 million remote sensing image-text pairs in total, covering more than 29K distinct semantic tags" to generate embeddings for 200x200 image segments (with 100px of overlap), then stored them in Pinecone.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/embeddings"&gt;embeddings&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/clip"&gt;clip&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="embeddings"/><category term="clip"/></entry><entry><title>tiny-world-map</title><link href="https://simonwillison.net/2024/Apr/21/tiny-world-map/#atom-tag" rel="alternate"/><published>2024-04-21T22:11:14+00:00</published><updated>2024-04-21T22:11:14+00:00</updated><id>https://simonwillison.net/2024/Apr/21/tiny-world-map/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tinyworldmap/tiny-world-map"&gt;tiny-world-map&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I love this project. It’s a JavaScript file (694K uncompressed, 283KB compressed) which can be used with the Leaflet mapping library and provides a SVG base map of the world with country borders and labels for every world city with a population more than 48,000—10,000 cities total.&lt;/p&gt;

&lt;p&gt;This means you can bundle an offline map of the world as part of any application that doesn’t need a higher level of detail. A lot of smaller island nations are missing entirely though, so this may not be right for every project.&lt;/p&gt;

&lt;p&gt;It even includes a service worker to help implement offline mapping support, plus several variants of the map with less cities that are even smaller.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=40104774"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mapping"&gt;mapping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/svg"&gt;svg&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/service-workers"&gt;service-workers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="javascript"/><category term="mapping"/><category term="svg"/><category term="service-workers"/><category term="leaflet"/></entry><entry><title>A POI Database in One Line</title><link href="https://simonwillison.net/2024/Apr/19/a-poi-database-in-one-line/#atom-tag" rel="alternate"/><published>2024-04-19T02:44:58+00:00</published><updated>2024-04-19T02:44:58+00:00</updated><id>https://simonwillison.net/2024/Apr/19/a-poi-database-in-one-line/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.dbreunig.com/2024/04/18/a-poi-database-in-one-line.html"&gt;A POI Database in One Line&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Overture maps offer an extraordinarily useful freely licensed databases of POI (point of interest) listings, principally derived from partners such as Facebook and including restaurants, shops, museums and other locations from all around the world.&lt;/p&gt;
&lt;p&gt;Their new "overturemaps" Python CLI utility makes it easy to quickly pull subsets of their data... but requires you to provide a bounding box to do so.&lt;/p&gt;
&lt;p&gt;Drew Breunig came up with this delightful recipe for fetching data using LLM and gpt-3.5-turbo to fill in those bounding boxes:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;overturemaps download --bbox=$(llm 'Give me a bounding box for Alameda, California expressed as only four numbers delineated by commas, with no spaces, longitude preceding latitude.') -f geojsonseq --type=place | geojson-to-sqlite alameda.db places - --nl --pk=id&lt;/code&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/dbreunig/status/1781133877320523792"&gt;@dbreunig&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geojson"&gt;geojson&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/drew-breunig"&gt;drew-breunig&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/overture"&gt;overture&lt;/a&gt;&lt;/p&gt;



</summary><category term="cli"/><category term="geospatial"/><category term="geojson"/><category term="llm"/><category term="drew-breunig"/><category term="overture"/></entry><entry><title>mapshaper.org</title><link href="https://simonwillison.net/2024/Mar/23/mapshaperorg/#atom-tag" rel="alternate"/><published>2024-03-23T03:44:22+00:00</published><updated>2024-03-23T03:44:22+00:00</updated><id>https://simonwillison.net/2024/Mar/23/mapshaperorg/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://mapshaper.org/"&gt;mapshaper.org&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
It turns out the mapshaper CLI tool for manipulating geospatial data—including converting shapefiles to GeoJSON and back again—also has a web UI that runs the conversions entirely in your browser. If you need to convert between those (and other) formats it’s hard to imagine a more convenient option.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/adamrpearce/status/1771378836128854097"&gt;@adamrpearce&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/shapefiles"&gt;shapefiles&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geojson"&gt;geojson&lt;/a&gt;&lt;/p&gt;



</summary><category term="cli"/><category term="geospatial"/><category term="javascript"/><category term="shapefiles"/><category term="geojson"/></entry><entry><title>Claude and ChatGPT for ad-hoc sidequests</title><link href="https://simonwillison.net/2024/Mar/22/claude-and-chatgpt-case-study/#atom-tag" rel="alternate"/><published>2024-03-22T19:44:12+00:00</published><updated>2024-03-22T19:44:12+00:00</updated><id>https://simonwillison.net/2024/Mar/22/claude-and-chatgpt-case-study/#atom-tag</id><summary type="html">
    &lt;p&gt;Here is a short, illustrative example of one of the ways in which I use Claude and ChatGPT on a daily basis.&lt;/p&gt;
&lt;p&gt;I recently learned that the &lt;a href="https://en.wikipedia.org/wiki/Adirondack_Park"&gt;Adirondack Park&lt;/a&gt; is the single largest park in the contiguous United States, taking up a fifth of the state of New York.&lt;/p&gt;
&lt;p&gt;Naturally, my first thought was that it would be neat to have a GeoJSON file representing the boundary of the park.&lt;/p&gt;
&lt;p&gt;A quick search landed me on the &lt;a href="https://apa.ny.gov/gis/ApaData.html"&gt;Adirondack Park Agency GIS data page&lt;/a&gt;, which offered me a shapefile of the "Outer boundary of the New York State Adirondack Park as described in Section 9-0101 of the New York Environmental Conservation Law". Sounds good!&lt;/p&gt;
&lt;p&gt;I knew there were tools for converting shapefiles to GeoJSON, but I couldn't remember what they were. Since I had a terminal window open already, I typed the following:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre style="white-space: pre-wrap"&gt;llm -m opus -c &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;give me options on macOS for CLI tools to turn a shapefile into GeoJSON&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here I am using my &lt;a href="https://llm.datasette.io/"&gt;LLM tool&lt;/a&gt; (and &lt;a href="https://github.com/simonw/llm-claude-3"&gt;llm-claude-3&lt;/a&gt; plugin) to run a prompt through the new &lt;a href="https://www.anthropic.com/news/claude-3-family"&gt;Claude 3 Opus&lt;/a&gt;, my current favorite language model.&lt;/p&gt;
&lt;p&gt;It &lt;a href="https://gist.github.com/simonw/331918e46f33e27e997afb4e7c62fc74"&gt;replied with a couple of options&lt;/a&gt;, but the first was this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;ogr2ogr -f GeoJSON output.geojson input.shp&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So I ran that against the shapefile, and then pasted &lt;a href="https://gist.github.com/simonw/c941f3454cdec7e10f500dc5a752b614"&gt;the resulting GeoJSON&lt;/a&gt; into &lt;a href="https://geojson.io/"&gt;geojson.io&lt;/a&gt; to check if it worked... and nothing displayed. Then I looked at the GeoJSON and spotted this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;"coordinates": [ [ -8358911.527799999341369, 5379193.197800002992153 ] ...&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That didn't look right. Those co-ordinates aren't the correct scale for latitude and longitude values.&lt;/p&gt;
&lt;p&gt;So I sent a follow-up prompt to the model (the &lt;code&gt;-c&lt;/code&gt; option means "continue previous conversation"):&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre style="white-space: pre-wrap"&gt;llm -c &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;i tried using ogr2ogr but it gave me back GeoJSON with a weird coordinate system that was not lat/lon that i am used to&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It suggested this new command:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;ogr2ogr -f GeoJSON -t_srs EPSG:4326 output.geojson input.shp&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This time &lt;a href="https://gist.github.com/simonw/6c4cf102a8ea532dc365c2773f0eb6ea"&gt;it worked&lt;/a&gt;! The shapefile has now been converted to GeoJSON.&lt;/p&gt;
&lt;p&gt;Time elapsed so far: 2.5 minutes (I can tell from &lt;a href="https://llm.datasette.io/en/stable/logging.html"&gt;my LLM logs&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I pasted it into &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; (with &lt;a href="https://github.com/datasette/datasette-paste"&gt;datasette-paste&lt;/a&gt; and &lt;a href="https://datasette.io/plugins/datasette-leaflet-geojson"&gt;datasette-leaflet-geojson&lt;/a&gt;) to take a look at it more closely, and got this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/datasette-adirondack-boundaries.jpg" alt="A Datasette table with 106 rows. The first two are shown - both have properties and a geometry, and the geometry is a single line on a map. The first one has a ECL_Text of thence southerly along the westerly line of lots 223, 241, 259, 276, 293, 309, 325 and 340 to the southwesterly corner of lot number 340 in the Brantingham Tract and the second has thence westerly along the northern line of lots 204 and 203 to the midpoint of the northern line of lot 203" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;That's not a single polygon! That's 106 line segments... and they are fascinating. Look at those descriptions:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;thence westerly along the northern line of lots 204 and 203 to the midpoint of the northern line of lot 203&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is utterly delightful. The shapefile description did say "as described in Section 9-0101 of the New York Environmental Conservation Law", so I guess this is how you write geographically boundaries into law!&lt;/p&gt;
&lt;p&gt;But it's not what I wanted. I want a single polygon of the whole park, not 106 separate lines.&lt;/p&gt;
&lt;p&gt;I decided to switch models. ChatGPT has access to Code Interpreter, and I happen to know that Code Interpreter is quite effective at processing GeoJSON.&lt;/p&gt;
&lt;p&gt;I opened a new ChatGPT (with GPT-4) browser tab, uploaded my GeoJSON file and prompted it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This GeoJSON file is full of line segments. Use them to create me a single shape that is a Polygon&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-chatgpt-1.jpg" alt="ChatGPT screenshot - it shows some Python code with a result of &amp;lt;shapely.geometry.polygon.Polygon at 0x7eba83f9fca0 /&amp;gt;, then says: I've created a polygon from the line segments in the GeoJSON file. You can now use this polygon for further analysis or visualization. If you have specific requirements for the polygon or need it in a particular format, please let me know! ​​" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;OK, so it wrote some Python code and ran it. But did it work?&lt;/p&gt;
&lt;p&gt;I happen to know that Code Interpreter can save files to disk and provide links to download them, so I told it to do that:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Save it to a GeoJSON file for me to download&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-chatgpt-2.jpg" alt="ChatGPT screenshot - this time it writes more Python code to define a GeoJSON polygon, then saves that to a file called /mnt/data/polygon.geojson and gives me a link to download it.​​" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;I pasted &lt;a href="https://gist.github.com/simonw/c1002dbf5249de7addd0b65cb774d3e9"&gt;that&lt;/a&gt; into &lt;a href="https://geojson.io/"&gt;geojson.io&lt;/a&gt;, and it was clearly wrong:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-chatgpt-bad-map.jpg" alt="geojson.io screenshot - a triangle shape sits on top of an area of upstate New York, clearly not in the shape of the park" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;So I told it to try again. I didn't think very hard about this prompt, I basically went with a version of "do better":&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;that doesn't look right to me, check that it has all of the lines in it&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-chatgpt-3.jpg" alt="ChatGPT screenshot - it writes more Python code and outputs a link to complete_polygon.geojson​​" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;It gave me a new file, optimistically named &lt;code&gt;complete_polygon.geojson&lt;/code&gt;. Here's what that one looked like:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-chatgpt-almost.jpg" alt="ChatGPT screenshot - it writes more Python code and outputs a link to complete_polygon.geojson​​" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This is getting a lot closer! Note how the right hand boundary of the park looks correct, but the rest of the image is scrambled.&lt;/p&gt;
&lt;p&gt;I had a hunch about the fix. I pasted in a screenshot of where we were so far and added my hunch about the solution:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;That almost works but you need to sort the line segments first, it looked like this:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Honestly, pasting in the screenshot probably wasn't necessary here, but it amused me.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-chatgpt-4.jpg" alt="That almost works but you need to sort the line segments first, it looked like this: an a screenshot of a map" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;... and ChatGPT churned away again ...&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-chatgpt-5.jpg" alt="More Python code - link to the full transcript is below" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://gist.github.com/simonw/b9e4325b76e4a3813ff5482aa278c342"&gt;sorted_polygon.geojson&lt;/a&gt; is spot on! Here's what it looks like:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-good-map.jpg" alt="A shaded polygon showing the exact shape of the boundary of Adirondack Park, overlayed on a map of the area" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Total time spent in ChatGPT: 3 minutes and 35 seconds. Plus 2.5 minutes with Claude 3 earlier, so an overall total of just over 6 minutes.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/0343cdd3568bbe28cad15d1097b1b1c7"&gt;the full Claude transcript&lt;/a&gt; and the &lt;a href="https://gist.github.com/simonw/3eb845823c5ad4c48d2b4eb7586f1533"&gt;full transcript from ChatGPT&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="not-notable"&gt;This isn't notable&lt;/h4&gt;
&lt;p&gt;The most notable thing about this example is how completely &lt;em&gt;not&lt;/em&gt; notable it is.&lt;/p&gt;
&lt;p&gt;I get results like this from these tools several times a day. I'm not at all surprised that this worked, in fact, I would've been mildly surprised if it had not.&lt;/p&gt;
&lt;p&gt;Could I have done this without LLM assistance? Yes, but not nearly as quickly. And this was not a task on my critical path for the day - it was a sidequest at best and honestly more of a distraction.&lt;/p&gt;
&lt;p&gt;So, without LLM tools, I would likely have given this one up at the first hurdle.&lt;/p&gt;
&lt;p&gt;A year ago I wrote about how &lt;a href="https://simonwillison.net/2023/Mar/27/ai-enhanced-development/"&gt;AI-enhanced development makes me more ambitious with my projects&lt;/a&gt;. They are now so firmly baked into my daily work that they influence not just side projects but tiny sidequests like this one as well.&lt;/p&gt;
&lt;h4 id="not-simple"&gt;This certainly wasn't simple&lt;/h4&gt;
&lt;p&gt;Something else I like about this example is that it illustrates quite how much depth there is to getting great results out of these systems.&lt;/p&gt;
&lt;p&gt;In those few minutes I used two different interfaces to call two different models. I sent multiple follow-up prompts. I triggered Code Interpreter, took advantage of GPT-4 Vision and mixed in external tools like &lt;a href="https://geojson.io/"&gt;geojson.io&lt;/a&gt; and Datasette as well.&lt;/p&gt;
&lt;p&gt;I leaned a lot on my existing knowledge and experience:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I knew that tools existed for commandline processing of shapefiles and GeoJSON&lt;/li&gt;
&lt;li&gt;I instinctively knew that Claude 3 Opus was likely to correctly answer my initial prompt&lt;/li&gt;
&lt;li&gt;I knew the capabilities of Code Interpreter, including that it has libraries that can process geometries, what to say to get it to kick into action and how to get it to give me files to download&lt;/li&gt;
&lt;li&gt;My limited GIS knowledge was strong enough to spot a likely coordinate system problem, and I guessed the fix for the jumbled lines&lt;/li&gt;
&lt;li&gt;My prompting intuition is developed to the point that I didn't have to think very hard about what to say to get the best results&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you have the right combination of domain knowledge and hard-won experience driving LLMs, you can &lt;em&gt;fly&lt;/em&gt; with these things.&lt;/p&gt;
&lt;h4 id="a-bit-trivial"&gt;Isn't this a bit trivial?&lt;/h4&gt;
&lt;p&gt;Yes it is, and that's the point. This was a five minute sidequest. Writing about it here took ten times longer than the exercise itself.&lt;/p&gt;
&lt;p&gt;I take on LLM-assisted sidequests like this one dozens of times a week. Many of them are substantially larger and more useful. They are having a very material impact on my work: I can get more done and solve much more interesting problems, because I'm not wasting valuable cycles figuring out &lt;code&gt;ogr2ogr&lt;/code&gt; invocations or mucking around with polygon libraries.&lt;/p&gt;
&lt;p&gt;Not to mention that I find working this way &lt;em&gt;fun&lt;/em&gt;! It feels like science fiction every time I do it. Our AI-assisted future is here right now and I'm still finding it weird, fascinating and deeply entertaining.&lt;/p&gt;
&lt;h4 id="llms-are-useful"&gt;LLMs are useful&lt;/h4&gt;
&lt;p&gt;There are many legitimate criticisms of LLMs. The copyright issues involved in their training, their enormous power consumption and the risks of people trusting them when they shouldn't (considering both accuracy and bias) are three that I think about a lot.&lt;/p&gt;
&lt;p&gt;The one criticism I wont accept is that they aren't &lt;em&gt;useful&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;One of the greatest misconceptions concerning LLMs is the idea that they are easy to use. They really aren't: getting great results out of them requires a great deal of experience and hard-fought intuition, combined with deep domain knowledge of the problem you are applying them to.&lt;/p&gt;
&lt;p&gt;I use these things every day. They help me take on much more interesting and ambitious problems than I could otherwise. I would miss them terribly if they were no longer available to me.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/shapefiles"&gt;shapefiles&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geojson"&gt;geojson&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chatgpt"&gt;chatgpt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/code-interpreter"&gt;code-interpreter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="geospatial"/><category term="shapefiles"/><category term="geojson"/><category term="ai"/><category term="openai"/><category term="generative-ai"/><category term="chatgpt"/><category term="llms"/><category term="ai-assisted-programming"/><category term="anthropic"/><category term="claude"/><category term="code-interpreter"/><category term="coding-agents"/><category term="leaflet"/></entry><entry><title>How to make self-hosted maps that work everywhere and cost next to nothing</title><link href="https://simonwillison.net/2024/Feb/24/self-hosted-maps/#atom-tag" rel="alternate"/><published>2024-02-24T04:19:05+00:00</published><updated>2024-02-24T04:19:05+00:00</updated><id>https://simonwillison.net/2024/Feb/24/self-hosted-maps/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.muckrock.com/news/archives/2024/feb/13/release-notes-how-to-make-self-hosted-maps-that-work-everywhere-cost-next-to-nothing-and-might-even-work-in-airplane-mode/"&gt;How to make self-hosted maps that work everywhere and cost next to nothing&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Chris Amico provides a detailed roundup of the state of web mapping in 2024. It’s never been easier to entirely host your own mapping infrastructure, thanks to OpenStreetMap, Overture, MBTiles, PMTiles, Maplibre and a whole ecosystem of other fine open source projects.&lt;/p&gt;

&lt;p&gt;I like Protomaps creator Brandon Liu’s description of this: “post-scarcity web mapping”.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/maps"&gt;maps&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openstreetmap"&gt;openstreetmap&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chris-amico"&gt;chris-amico&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/overture"&gt;overture&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="maps"/><category term="openstreetmap"/><category term="chris-amico"/><category term="overture"/></entry><entry><title>Database generated columns: GeoDjango &amp; PostGIS</title><link href="https://simonwillison.net/2023/Dec/11/database-generated-columns-geodjango/#atom-tag" rel="alternate"/><published>2023-12-11T19:14:28+00:00</published><updated>2023-12-11T19:14:28+00:00</updated><id>https://simonwillison.net/2023/Dec/11/database-generated-columns-geodjango/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.paulox.net/2023/12/11/database-generated-columns-part-3-geodjango-and-postgis/"&gt;Database generated columns: GeoDjango &amp;amp; PostGIS&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Paolo Melchiorre advocated for the inclusion of generated columns, one of the biggest features in Django 5.0. Here he provides a detailed tutorial showing how they can be used with PostGIS to create database tables that offer columns such as geohash that are automatically calculated from other columns in the table.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="geospatial"/><category term="postgresql"/></entry><entry><title>Geospatial SQL queries in SQLite using TG, sqlite-tg and datasette-sqlite-tg</title><link href="https://simonwillison.net/2023/Sep/25/tg-sqlite/#atom-tag" rel="alternate"/><published>2023-09-25T19:45:03+00:00</published><updated>2023-09-25T19:45:03+00:00</updated><id>https://simonwillison.net/2023/Sep/25/tg-sqlite/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/sqlite/sqlite-tg"&gt;Geospatial SQL queries in SQLite using TG, sqlite-tg and datasette-sqlite-tg&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Alex Garcia built sqlite-tg—a SQLite extension that uses the brand new TG geospatial library to provide a whole suite of custom SQL functions for working with geospatial data.&lt;/p&gt;

&lt;p&gt;Here are my notes on trying out his initial alpha releases. The extension already provides tools for converting between GeoJSON, WKT and WKB, plus the all important tg_intersects() function for testing if a polygon or point overlap each other.&lt;/p&gt;

&lt;p&gt;It’s pretty useful already. Without any geospatial indexing at all I was still able to get 700ms replies to a brute-force point-in-polygon query against 150MB of GeoJSON timezone boundaries stored as JSON text in a table.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geojson"&gt;geojson&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/alex-garcia"&gt;alex-garcia&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tg"&gt;tg&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="sqlite"/><category term="geojson"/><category term="datasette"/><category term="alex-garcia"/><category term="tg"/></entry><entry><title>TG: Polygon indexing</title><link href="https://simonwillison.net/2023/Sep/23/tg-polygon-indexing/#atom-tag" rel="alternate"/><published>2023-09-23T04:32:14+00:00</published><updated>2023-09-23T04:32:14+00:00</updated><id>https://simonwillison.net/2023/Sep/23/tg-polygon-indexing/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tidwall/tg/blob/v0.1.0/docs/POLYGON_INDEXING.md"&gt;TG: Polygon indexing&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
TG is a brand new geospatial library by Josh Baker, author of the Tile38 in-memory spatial server (kind of a geospatial Redis). TG is written in pure C and delivered as a single C file, reminiscent of the SQLite amalgamation.&lt;/p&gt;

&lt;p&gt;TG looks really interesting. It implements almost the exact subset of geospatial functionality that I find most useful: point-in-polygon, intersect, WKT, WKB, and GeoJSON—all with no additional dependencies.&lt;/p&gt;

&lt;p&gt;The most interesting thing about it is the way it handles indexing. In this documentation Josh describes two approaches he uses to speeding up point-in-polygon and intersection using a novel approach that goes beyond the usual RTree implementation.&lt;/p&gt;

&lt;p&gt;I think this could make the basis of a really useful SQLite extension—a lighter-weight alternative to SpatiaLite.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=37619100"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/c"&gt;c&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/spatialite"&gt;spatialite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geojson"&gt;geojson&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tg"&gt;tg&lt;/a&gt;&lt;/p&gt;



</summary><category term="c"/><category term="geospatial"/><category term="spatialite"/><category term="sqlite"/><category term="geojson"/><category term="tg"/></entry><entry><title>Overture Maps Foundation Releases Its First World-Wide Open Map Dataset</title><link href="https://simonwillison.net/2023/Jul/27/overture-maps/#atom-tag" rel="alternate"/><published>2023-07-27T16:45:09+00:00</published><updated>2023-07-27T16:45:09+00:00</updated><id>https://simonwillison.net/2023/Jul/27/overture-maps/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://overturemaps.org/overture-maps-foundation-releases-first-world-wide-open-map-dataset/"&gt;Overture Maps Foundation Releases Its First World-Wide Open Map Dataset&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The Overture Maps Foundation is a collaboration lead by Amazon, Meta, Microsoft and TomTom dedicated to producing “reliable, easy-to-use, and interoperable open map data”.&lt;/p&gt;

&lt;p&gt;Yesterday they put out their first release and it’s pretty astonishing: four different layers of geodata, covering Places of Interest (shops, restaurants, attractions etc), administrative boundaries, building outlines and transportation networks.&lt;/p&gt;

&lt;p&gt;The data is available as Parquet. I just downloaded the 8GB places dataset and can confirm that it contains 59 million listings from around the world—I filtered to just places in my local town and a spot check showed that recently opened businesses (last 12 months) were present and the details all looked accurate.&lt;/p&gt;

&lt;p&gt;The places data is licensed under “Community Data License Agreement – Permissive” which looks like the only restriction is that you have to include that license when you further share the data.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-data"&gt;open-data&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/parquet"&gt;parquet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/meta"&gt;meta&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/overture"&gt;overture&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="open-data"/><category term="parquet"/><category term="meta"/><category term="overture"/></entry><entry><title>GPSJam</title><link href="https://simonwillison.net/2022/Jul/30/gpsjam/#atom-tag" rel="alternate"/><published>2022-07-30T19:51:31+00:00</published><updated>2022-07-30T19:51:31+00:00</updated><id>https://simonwillison.net/2022/Jul/30/gpsjam/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://gpsjam.org/"&gt;GPSJam&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
John Wiseman’s “Daily maps of GPS interference” —a beautiful interactive globe (powered by Mapbox GL) which you can use to see points of heaviest GPS interference over a 24 hour period, using data collected from commercial airline radios by ADS-B Exchange. “From what I can tell the most common reason for aircraft GPS systems to have degraded accuracy is jamming by military systems. At least, the vast majority of aircraft that I see with bad GPS accuracy are flying near conflict zones where GPS jamming is known to occur.”

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/lemonodor/status/1553097820860600320"&gt;@lemonodor&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gps"&gt;gps&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mapping"&gt;mapping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/john-wiseman"&gt;john-wiseman&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="gps"/><category term="mapping"/><category term="john-wiseman"/></entry><entry><title>A tiny web app to create images from OpenStreetMap maps</title><link href="https://simonwillison.net/2022/Jun/12/url-map/#atom-tag" rel="alternate"/><published>2022-06-12T05:49:35+00:00</published><updated>2022-06-12T05:49:35+00:00</updated><id>https://simonwillison.net/2022/Jun/12/url-map/#atom-tag</id><summary type="html">
    &lt;p&gt;Earlier today I found myself wanting to programmatically generate some images of maps.&lt;/p&gt;
&lt;p&gt;I wanted to create a map centered around a location, at a specific zoom level, and with a marker in a specific place.&lt;/p&gt;
&lt;p&gt;Some cursory searches failed to turn up exactly what I wanted, so I decided to build a tiny project to solve the problem, taking advantage of my &lt;a href="https://shot-scraper.datasette.io/"&gt;shot-scraper tool&lt;/a&gt; for automating screenshots of web pages.&lt;/p&gt;
&lt;p&gt;The result is &lt;a href="https://map.simonwillison.net/"&gt;map.simonwillison.net&lt;/a&gt; - hosted on GitHub Pages from my &lt;a href="https://github.com/simonw/url-map"&gt;simonw/url-map&lt;/a&gt; repository.&lt;/p&gt;
&lt;p&gt;Here's how to generate a map image of Washington DC:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shot-scraper 'https://map.simonwillison.net/?q=washington+dc' \
  --retina --width 600 --height 400 --wait 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That command generates a PNG 1200x800 image that's a retina screenshot of the map displayed at &lt;a href="https://map.simonwillison.net/?q=washington+dc"&gt;https://map.simonwillison.net/?q=washington+dc&lt;/a&gt; - after waiting three seconds to esure all of the tiles have fully loaded.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/map-simonwillison-net.jpg" alt="A map of Washington DC, with a Leaflet / OpenStreetMap attribution in the bottom right" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;The website itself is &lt;a href="https://github.com/simonw/url-map/blob/main/README.md"&gt;documented here&lt;/a&gt;. It displays a map with no visible controls, though you can use gestures to zoom in and pan around - and the URL bar will update to reflect your navigation, so you can bookmark or share the URL once you've got it to the right spot.&lt;/p&gt;
&lt;p&gt;You can also use query string parameters to specify the map that should be initially displayed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://map.simonwillison.net/?center=51.49,0&amp;amp;zoom=8"&gt;https://map.simonwillison.net/?center=51.49,0&amp;amp;zoom=8&lt;/a&gt; displays a map at zoom level 8 centered on the specified latitude, longitude coordinate pair.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://map.simonwillison.net/?q=islington+london"&gt;https://map.simonwillison.net/?q=islington+london&lt;/a&gt; geocodes the &lt;code&gt;?q=&lt;/code&gt; text using &lt;a href="https://nominatim.openstreetmap.org/ui/search.html"&gt;OpenStreetMap Nominatim&lt;/a&gt; and zooms to the level that best fits the bounding box of the first returned result.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://map.simonwillison.net/?q=islington+london&amp;amp;zoom=12"&gt;https://map.simonwillison.net/?q=islington+london&amp;amp;zoom=12&lt;/a&gt; does that but zooms to level 12 instead of using the best fit for the bounding box&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://map.simonwillison.net/?center=51.49,0&amp;amp;zoom=8&amp;amp;marker=51.49,0&amp;amp;marker=51.3,0.2"&gt;https://map.simonwillison.net/?center=51.49,0&amp;amp;zoom=8&amp;amp;marker=51.49,0&amp;amp;marker=51.3,0.2&lt;/a&gt; adds two blue markers to the specified map. You can pass &lt;code&gt;&amp;amp;marker=lat,lon&lt;/code&gt; as many times as you like to add multiple markers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Annotated source code&lt;/h4&gt;
&lt;p&gt;The entire mapping application is contained in a single 68 line &lt;code&gt;index.html&lt;/code&gt; file that mixes HTML and JavaScript. It's built using the fantastic &lt;a href="https://leafletjs.com/"&gt;Leaflet&lt;/a&gt; open source mapping library.&lt;/p&gt;
&lt;p&gt;Since the code is so short, I'll enclude the entire thing here with some additional annotating comments.&lt;/p&gt;
&lt;p&gt;It started out as a copy of the first example in &lt;a href="https://leafletjs.com/examples/quick-start/"&gt;the Leaflet quick start guide&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight highlight-text-html-basic"&gt;&lt;pre&gt;&lt;span class="pl-c1"&gt;&amp;lt;!DOCTYPE html&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&amp;lt;!-- Regular HTML boilerplate --&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;html&lt;/span&gt; &lt;span class="pl-c1"&gt;lang&lt;/span&gt;="&lt;span class="pl-s"&gt;en&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;head&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;meta&lt;/span&gt; &lt;span class="pl-c1"&gt;charset&lt;/span&gt;="&lt;span class="pl-s"&gt;utf-8&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;meta&lt;/span&gt; &lt;span class="pl-c1"&gt;name&lt;/span&gt;="&lt;span class="pl-s"&gt;viewport&lt;/span&gt;" &lt;span class="pl-c1"&gt;content&lt;/span&gt;="&lt;span class="pl-s"&gt;width=device-width, initial-scale=1&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;title&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;map.simonwillison.net&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;title&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&amp;lt;!--&lt;/span&gt;
&lt;span class="pl-c"&gt;  Leaflet's CSS and JS are loaded from the unpgk.com CDN, with the&lt;/span&gt;
&lt;span class="pl-c"&gt;  Subresource Integrity (SRI) integrity="sha512..." attribute to ensure&lt;/span&gt;
&lt;span class="pl-c"&gt;  that the exact expected code is served by the CDN.&lt;/span&gt;
&lt;span class="pl-c"&gt;--&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;link&lt;/span&gt; &lt;span class="pl-c1"&gt;rel&lt;/span&gt;="&lt;span class="pl-s"&gt;stylesheet&lt;/span&gt;" &lt;span class="pl-c1"&gt;href&lt;/span&gt;="&lt;span class="pl-s"&gt;https://unpkg.com/leaflet@1.8.0/dist/leaflet.css&lt;/span&gt;" &lt;span class="pl-c1"&gt;integrity&lt;/span&gt;="&lt;span class="pl-s"&gt;sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ==&lt;/span&gt;" &lt;span class="pl-c1"&gt;crossorigin&lt;/span&gt;=""/&amp;gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt; &lt;span class="pl-c1"&gt;src&lt;/span&gt;="&lt;span class="pl-s"&gt;https://unpkg.com/leaflet@1.8.0/dist/leaflet.js&lt;/span&gt;" &lt;span class="pl-c1"&gt;integrity&lt;/span&gt;="&lt;span class="pl-s"&gt;sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ==&lt;/span&gt;" &lt;span class="pl-c1"&gt;crossorigin&lt;/span&gt;=""&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&amp;lt;!-- I want the map to occupy the entire browser window with no margins --&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;style&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-ent"&gt;html&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-ent"&gt;body&lt;/span&gt; {
  &lt;span class="pl-c1"&gt;height&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;100&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt;;
  &lt;span class="pl-c1"&gt;margin&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;/span&gt;;
}
&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;style&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;head&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;body&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&amp;lt;!-- The Leaflet map renders in this 100% high/wide div --&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;div&lt;/span&gt; &lt;span class="pl-c1"&gt;id&lt;/span&gt;="&lt;span class="pl-s"&gt;map&lt;/span&gt;" &lt;span class="pl-c1"&gt;style&lt;/span&gt;="&lt;span class="pl-s"&gt;width: 100%; height: 100%;&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;div&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;toPoint&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;s&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Convert "51.5,2.1" into [51.5, 2.1]&lt;/span&gt;
  &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;s&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;split&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;","&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;parseFloat&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-c"&gt;// An async function so we can 'await fetch(...)' later on&lt;/span&gt;
&lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;load&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c"&gt;// URLSearchParams is a fantastic browser API - it makes it easy to both read&lt;/span&gt;
  &lt;span class="pl-c"&gt;// query string parameters from the URL and later to generate new ones&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;params&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;URLSearchParams&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;location&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;search&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// If the starting URL is /?center=51,32&amp;amp;zoom=3 this will pull those values out&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;center&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;params&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;get&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'center'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;'0,0'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;initialZoom&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;params&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;get&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'zoom'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;zoom&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;parseInt&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;initialZoom&lt;/span&gt; &lt;span class="pl-c1"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;'2'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;10&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;q&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;params&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;get&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'q'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// .getAll() turns &amp;amp;marker=51.49,0&amp;amp;marker=51.3,0.2 into ['51.49,0', '51.3,0.2']&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;markers&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;params&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getAll&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'marker'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// zoomControl: false turns off the visible +/- zoom buttons in Leaflet&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;map&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;L&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'map'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;zoomControl&lt;/span&gt;: &lt;span class="pl-c1"&gt;false&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;setView&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-en"&gt;toPoint&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;center&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;zoom&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-v"&gt;L&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;tileLayer&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;maxZoom&lt;/span&gt;: &lt;span class="pl-c1"&gt;19&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;attribution&lt;/span&gt;: &lt;span class="pl-s"&gt;'&amp;amp;copy; &amp;lt;a href="http://www.openstreetmap.org/copyright"&amp;gt;OpenStreetMap&amp;lt;/a&amp;gt;'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c"&gt;// This option means retina-capable devices will get double-resolution tiles:&lt;/span&gt;
    &lt;span class="pl-c1"&gt;detectRetina&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addTo&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// We only pay attention to ?q= if ?center= was not provided:&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;q&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="pl-c1"&gt;!&lt;/span&gt;&lt;span class="pl-s1"&gt;params&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;get&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'center'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c"&gt;// We use fetch to pass ?q= to the Nominatim API and get back JSON&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
      &lt;span class="pl-s"&gt;`https://nominatim.openstreetmap.org/search.php?q=&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-en"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;q&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;&amp;amp;format=jsonv2`&lt;/span&gt;
    &lt;span class="pl-kos"&gt;)&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;data&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// data[0] is the first result - it has a boundingbox array of four floats&lt;/span&gt;
    &lt;span class="pl-c"&gt;// which we can convert into a Leaflet-compatible bounding box like this:&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;bounds&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;[&lt;/span&gt;
      &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;boundingbox&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;&lt;span class="pl-s1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;boundingbox&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;2&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;boundingbox&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;1&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;&lt;span class="pl-s1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;boundingbox&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;3&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;
    &lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// This sets both the map center and zooms to the correct level for the bbox:&lt;/span&gt;
    &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;fitBounds&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;bounds&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// User-provided zoom over-rides this&lt;/span&gt;
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;initialZoom&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;setZoom&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-en"&gt;parseInt&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;initialZoom&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-c"&gt;// This is the code that updates the URL as the user pans or zooms around.&lt;/span&gt;
  &lt;span class="pl-c"&gt;// You can subscribe to both the moveend and zoomend Leaflet events in one go:&lt;/span&gt;
  &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;on&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'moveend zoomend'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c"&gt;// Update URL bar with current location&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;newZoom&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getZoom&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;center&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getCenter&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// This time we use URLSearchParams to construct a center...=&amp;amp;zoom=... URL&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;u&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;URLSearchParams&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// Copy across ?marker=x&amp;amp;marker=y from existing URL, if they were set:&lt;/span&gt;
    &lt;span class="pl-s1"&gt;markers&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;forEach&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;s&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;u&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;append&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'marker'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;s&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;u&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;append&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'center'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;`&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;center&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;lat&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;,&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;center&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;lng&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;`&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;u&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;append&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'zoom'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;newZoom&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// replaceState() is a weird API - the third argument is the one we care about:&lt;/span&gt;
    &lt;span class="pl-s1"&gt;history&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;replaceState&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-c1"&gt;null&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;null&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;'?'&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s1"&gt;u&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;toString&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// This bit adds Leaflet markers to the map for ?marker= query string arguments:&lt;/span&gt;
  &lt;span class="pl-s1"&gt;markers&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;forEach&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;s&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-v"&gt;L&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;marker&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-en"&gt;toPoint&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;s&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addTo&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-en"&gt;load&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;body&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;html&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&amp;lt;!-- See https://github.com/simonw/url-map for documentation --&amp;gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/maps"&gt;maps&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openstreetmap"&gt;openstreetmap&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/shot-scraper"&gt;shot-scraper&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="geospatial"/><category term="maps"/><category term="openstreetmap"/><category term="projects"/><category term="shot-scraper"/><category term="leaflet"/></entry><entry><title>Datasette for geospatial analysis</title><link href="https://simonwillison.net/2022/Apr/13/datasette-for-geospatial-analysis/#atom-tag" rel="alternate"/><published>2022-04-13T00:48:14+00:00</published><updated>2022-04-13T00:48:14+00:00</updated><id>https://simonwillison.net/2022/Apr/13/datasette-for-geospatial-analysis/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://datasette.io/for/geospatial"&gt;Datasette for geospatial analysis&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I added a new page to the Datasette website describing how Datasette can be used for geospatial analysis, pulling together several of the relevant plugins and tools from the Datasette ecosystem.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/simonw/status/1513968727821225985"&gt;@simonw&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="plugins"/><category term="datasette"/></entry></feed>