Initial version
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
If you need to create a map on Wordpress with around many markers, the following script should help you.
|
||||
|
||||
The Python script will:
|
||||
- Read `name,address` rows from a CSV file.
|
||||
- Send each address to Mapbox’s forward-geocoding API.
|
||||
- Extract the returned place name, Mapbox feature ID, longitude, and latitude.
|
||||
- Generate a complete `wp:jetpack/map` block that you can paste into the WordPress code editor.
|
||||
|
||||
Mapbox’s geocoder converts address text into coordinates, and its response includes a feature ID and human-readable place name suitable for constructing the Jetpack point objects.
|
||||
|
||||
## Expected CSV
|
||||
|
||||
Save a file called `addresses.csv`:
|
||||
|
||||
```csv
|
||||
name,address
|
||||
Alice,"1 rue de la Paix, Paris, France"
|
||||
Bob,"3 avenue Foch, Paris, France"
|
||||
```
|
||||
|
||||
The first column becomes the marker title. The second column is sent to Mapbox.
|
||||
|
||||
## Running it
|
||||
|
||||
Install the required library:
|
||||
|
||||
```bash
|
||||
python -m pip install requests
|
||||
```
|
||||
|
||||
Set your Mapbox token.
|
||||
|
||||
On macOS/Linux:
|
||||
|
||||
```bash
|
||||
export MAPBOX_TOKEN="pk.your_mapbox_token_here"
|
||||
```
|
||||
|
||||
On Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:MAPBOX_TOKEN="pk.your_mapbox_token_here"
|
||||
```
|
||||
|
||||
Run the script:
|
||||
|
||||
```bash
|
||||
python generate_jetpack_map.py addresses.csv
|
||||
```
|
||||
|
||||
It will create:
|
||||
|
||||
```text
|
||||
jetpack-map.txt
|
||||
```
|
||||
|
||||
You can also specify the output filename:
|
||||
|
||||
```bash
|
||||
python generate_jetpack_map.py addresses.csv \
|
||||
--output wordpress-map.txt \
|
||||
--country fr \
|
||||
--language fr \
|
||||
--zoom 12
|
||||
```
|
||||
|
||||
The generated file will contain a block that you can paste into Wordpress. Wordpress will complain that the element needs to be repaired, but it should work.
|
||||
|
||||
The script calculates `mapCenter` as the average longitude and latitude of all successfully geocoded addresses.
|
||||
|
||||
A few practical details:
|
||||
|
||||
- `title` comes from the CSV `name` column.
|
||||
- `placeTitle` and `caption` come from Mapbox’s returned address.
|
||||
- Failed addresses are excluded from the map and written to a separate `*-failed.csv` file.
|
||||
- The token is read from an environment variable rather than being written into the script.
|
||||
- If your addresses are not in France, change `--country`, for example `--country be` or `--country us`.
|
||||
- If you do not want Mapbox to return French place names, use `--language en`.
|
||||
|
||||
The `permanent=True` setting in the script is intentional because the coordinates and Mapbox IDs are being saved into WordPress. If your Mapbox account does not support permanent geocoding, remove that setting only after confirming that your intended storage and use comply with Mapbox’s current terms.
|
||||
@@ -0,0 +1,3 @@
|
||||
name,address
|
||||
Alice,"1 rue de la Paix, Paris, France"
|
||||
Bob,"3 avenue Foch, Paris, France"
|
||||
|
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
MAPBOX_GEOCODING_URL = (
|
||||
"https://api.mapbox.com/geocoding/v5/mapbox.places"
|
||||
)
|
||||
|
||||
|
||||
def geocode_address(
|
||||
address,
|
||||
token,
|
||||
country=None,
|
||||
language="fr",
|
||||
permanent=True,
|
||||
):
|
||||
"""
|
||||
Return the best Mapbox result for an address.
|
||||
"""
|
||||
|
||||
url = f"{MAPBOX_GEOCODING_URL}/{quote(address)}.json"
|
||||
|
||||
params = {
|
||||
"access_token": token,
|
||||
"limit": 1,
|
||||
"language": language,
|
||||
"autocomplete": "false",
|
||||
}
|
||||
|
||||
if country:
|
||||
params["country"] = country
|
||||
|
||||
# Use permanent=true only when your Mapbox account permits
|
||||
# permanent storage of geocoding results.
|
||||
if permanent:
|
||||
params["permanent"] = "true"
|
||||
|
||||
response = requests.get(url, params=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
features = data.get("features", [])
|
||||
|
||||
if not features:
|
||||
return None
|
||||
|
||||
feature = features[0]
|
||||
coordinates = feature.get("center")
|
||||
|
||||
if not coordinates:
|
||||
geometry = feature.get("geometry", {})
|
||||
coordinates = geometry.get("coordinates")
|
||||
|
||||
if not coordinates or len(coordinates) < 2:
|
||||
return None
|
||||
|
||||
longitude = float(coordinates[0])
|
||||
latitude = float(coordinates[1])
|
||||
|
||||
return {
|
||||
"placeTitle": feature.get("place_name", address),
|
||||
"caption": feature.get("place_name", address),
|
||||
"id": feature.get("id", ""),
|
||||
"longitude": longitude,
|
||||
"latitude": latitude,
|
||||
}
|
||||
|
||||
|
||||
def read_csv_rows(csv_path):
|
||||
"""
|
||||
Read a CSV with columns:
|
||||
name,address
|
||||
"""
|
||||
|
||||
with open(csv_path, "r", encoding="utf-8-sig", newline="") as file:
|
||||
reader = csv.DictReader(file)
|
||||
|
||||
if not reader.fieldnames:
|
||||
raise ValueError("The CSV file has no header row.")
|
||||
|
||||
required_columns = {"name", "address"}
|
||||
actual_columns = set(reader.fieldnames)
|
||||
|
||||
missing = required_columns - actual_columns
|
||||
if missing:
|
||||
raise ValueError(
|
||||
f"Missing CSV column(s): {', '.join(sorted(missing))}. "
|
||||
"Expected columns: name,address"
|
||||
)
|
||||
|
||||
rows = []
|
||||
|
||||
for line_number, row in enumerate(reader, start=2):
|
||||
name = (row.get("name") or "").strip()
|
||||
address = (row.get("address") or "").strip()
|
||||
|
||||
if not name and not address:
|
||||
continue
|
||||
|
||||
if not name or not address:
|
||||
raise ValueError(
|
||||
f"Line {line_number}: both name and address are required."
|
||||
)
|
||||
|
||||
rows.append({
|
||||
"name": name,
|
||||
"address": address,
|
||||
})
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
def create_jetpack_points(rows, token, country=None, language="fr"):
|
||||
points = []
|
||||
failed = []
|
||||
|
||||
total = len(rows)
|
||||
|
||||
for index, row in enumerate(rows, start=1):
|
||||
print(
|
||||
f"Geocoding {index}/{total}: "
|
||||
f"{row['address']}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
try:
|
||||
result = geocode_address(
|
||||
address=row["address"],
|
||||
token=token,
|
||||
country=country,
|
||||
language=language,
|
||||
permanent=True,
|
||||
)
|
||||
except requests.RequestException as error:
|
||||
print(
|
||||
f" Request failed: {error}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
result = None
|
||||
|
||||
if result is None:
|
||||
failed.append(row)
|
||||
print(" No geocoding result found.", file=sys.stderr)
|
||||
continue
|
||||
|
||||
point = {
|
||||
"placeTitle": result["placeTitle"],
|
||||
"title": row["name"],
|
||||
"caption": result["caption"],
|
||||
"id": result["id"],
|
||||
"coordinates": {
|
||||
"longitude": result["longitude"],
|
||||
"latitude": result["latitude"],
|
||||
},
|
||||
}
|
||||
|
||||
points.append(point)
|
||||
|
||||
# A short delay avoids sending requests too quickly.
|
||||
time.sleep(0.1)
|
||||
|
||||
return points, failed
|
||||
|
||||
|
||||
def calculate_map_center(points):
|
||||
longitudes = [
|
||||
point["coordinates"]["longitude"]
|
||||
for point in points
|
||||
]
|
||||
|
||||
latitudes = [
|
||||
point["coordinates"]["latitude"]
|
||||
for point in points
|
||||
]
|
||||
|
||||
return {
|
||||
"lng": statistics.mean(longitudes),
|
||||
"lat": statistics.mean(latitudes),
|
||||
}
|
||||
|
||||
|
||||
def generate_jetpack_block(
|
||||
points,
|
||||
zoom=12.125807719593206,
|
||||
marker_color="#B50B3E",
|
||||
):
|
||||
if not points:
|
||||
raise ValueError("No valid geocoded points were found.")
|
||||
|
||||
map_center = calculate_map_center(points)
|
||||
|
||||
block_attributes = {
|
||||
"points": points,
|
||||
"zoom": zoom,
|
||||
"mapCenter": map_center,
|
||||
"markerColor": marker_color,
|
||||
"scrollToZoom": True,
|
||||
}
|
||||
|
||||
# separators=(",", ":") produces compact JSON.
|
||||
attributes_json = json.dumps(
|
||||
block_attributes,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
|
||||
return f"<!-- wp:jetpack/map {attributes_json} -->"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate a Jetpack map block from an address CSV."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"csv_file",
|
||||
help="CSV file containing name,address columns",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
default="jetpack-map.txt",
|
||||
help="Output file; default: jetpack-map.txt",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--token",
|
||||
default=os.environ.get("MAPBOX_TOKEN"),
|
||||
help="Mapbox access token, or use MAPBOX_TOKEN environment variable",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--country",
|
||||
default="fr",
|
||||
help="Two-letter country code used to bias results; default: fr",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--language",
|
||||
default="fr",
|
||||
help="Language for returned place names; default: fr",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--zoom",
|
||||
type=float,
|
||||
default=12.125807719593206,
|
||||
help="Map zoom level",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--marker-color",
|
||||
default="#B50B3E",
|
||||
help="Jetpack marker color",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.token:
|
||||
parser.error(
|
||||
"A Mapbox token is required. Use --token or set MAPBOX_TOKEN."
|
||||
)
|
||||
|
||||
rows = read_csv_rows(args.csv_file)
|
||||
|
||||
points, failed = create_jetpack_points(
|
||||
rows=rows,
|
||||
token=args.token,
|
||||
country=args.country,
|
||||
language=args.language,
|
||||
)
|
||||
|
||||
block = generate_jetpack_block(
|
||||
points=points,
|
||||
zoom=args.zoom,
|
||||
marker_color=args.marker_color,
|
||||
)
|
||||
|
||||
output_path = Path(args.output)
|
||||
output_path.write_text(block + "\n", encoding="utf-8")
|
||||
|
||||
print(
|
||||
f"\nGenerated {len(points)} map point(s) in {output_path}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if failed:
|
||||
failed_path = output_path.with_name(
|
||||
output_path.stem + "-failed.csv"
|
||||
)
|
||||
|
||||
with open(
|
||||
failed_path,
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
newline="",
|
||||
) as file:
|
||||
writer = csv.DictWriter(
|
||||
file,
|
||||
fieldnames=["name", "address"],
|
||||
)
|
||||
writer.writeheader()
|
||||
writer.writerows(failed)
|
||||
|
||||
print(
|
||||
f"{len(failed)} address(es) could not be geocoded.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
f"They were written to {failed_path}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user