
Geo Toolkit
- 116 installs
- 84 repo stars
- Updated April 8, 2026
- dkyazzentwatwa/chatgpt-skills
Optimize pages and copy so ChatGPT, Perplexity, and Google AI Overviews cite your product—entity maps, direct-answer rewrites, and llms.txt-style visibility tactics.
About
geo-toolkit from chatgpt-skills guides Claude Code through generative-engine optimization: rewriting pages for direct answers, mapping entities for citation, and applying llms.txt-style visibility patterns so SaaS and content sites get referenced by AI search products after launch.
- AI search citation optimization
- Direct-answer content rewrites
- Entity and citation mapping
- llms.txt and visibility hygiene
- Perplexity and ChatGPT discoverability
Geo Toolkit by the numbers
- 116 all-time installs (skills.sh)
- Ranked #1,103 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dkyazzentwatwa/chatgpt-skills --skill geo-toolkitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 116 |
|---|---|
| repo stars | ★ 84 |
| Last updated | April 8, 2026 |
| Repository | dkyazzentwatwa/chatgpt-skills ↗ |
What it does
Optimize pages and copy so ChatGPT, Perplexity, and Google AI Overviews cite your product—entity maps, direct-answer rewrites, and llms.txt-style visibility tactics.
Files
Geo Toolkit
Use this suite for practical geographic data preparation and inspection.
Included Tools
address_parser.pydistance_calc.pygeo_visualizer.pygeocoder.pykml_geojson_converter.pyterritory_mapper.py
Workflow
1. Determine whether the task is parsing, conversion, lookup, measurement, or visualization. 2. Normalize addresses or file formats before doing downstream mapping work. 3. Use the smallest tool that solves the request and return any geocoding or projection caveats.
Guardrails
- Treat geocoding results as approximate unless the source data is already precise.
- Call out coordinate system or file-format assumptions when converting map artifacts.
display_name: 'Geo Toolkit'
short_description: 'Parse, geocode, convert, and visualize geographic data.'
default_prompt: 'Help me work with this address, map file, or geo dataset.'
#!/usr/bin/env python3
"""
Address Parser - Parse unstructured addresses.
"""
import argparse
import re
import pandas as pd
class AddressParser:
"""Parse addresses into components."""
US_STATES = {
'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA',
'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD',
'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ',
'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC',
'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY'
}
def __init__(self):
"""Initialize parser."""
pass
def parse(self, address: str) -> dict:
"""Parse address into components."""
result = {
'original': address,
'street': None,
'city': None,
'state': None,
'zip': None,
'country': 'USA'
}
# Extract ZIP code
zip_match = re.search(r'\b\d{5}(?:-\d{4})?\b', address)
if zip_match:
result['zip'] = zip_match.group()
address = address.replace(zip_match.group(), '').strip()
# Extract state
for state in self.US_STATES:
if re.search(rf'\b{state}\b', address, re.IGNORECASE):
result['state'] = state
address = re.sub(rf'\b{state}\b', '', address, flags=re.IGNORECASE).strip()
break
# Split remaining by comma
parts = [p.strip() for p in address.split(',')]
if len(parts) >= 2:
result['street'] = parts[0]
result['city'] = parts[1]
elif len(parts) == 1:
result['street'] = parts[0]
# Clean up
for key in result:
if isinstance(result[key], str):
result[key] = result[key].strip().strip(',').strip()
return result
def parse_batch(self, addresses: list) -> pd.DataFrame:
"""Parse multiple addresses."""
results = [self.parse(addr) for addr in addresses]
return pd.DataFrame(results)
def main():
parser = argparse.ArgumentParser(description="Address Parser")
parser.add_argument("--input", "-i", required=True, help="Input CSV file")
parser.add_argument("--column", required=True, help="Address column name")
parser.add_argument("--output", "-o", required=True, help="Output CSV file")
args = parser.parse_args()
df = pd.read_csv(args.input)
addresses = df[args.column].tolist()
address_parser = AddressParser()
parsed = address_parser.parse_batch(addresses)
# Combine with original data
result = pd.concat([df, parsed.drop(columns=['original'])], axis=1)
result.to_csv(args.output, index=False)
print(f"Parsed {len(addresses)} addresses")
print(f"Output saved: {args.output}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Distance Calculator - Calculate geographic distances and find nearby points.
Features:
- Point-to-point distance (Haversine, Vincenty)
- Distance matrix
- Nearest neighbor search
- Radius search
- Multiple units
"""
import argparse
import csv
import math
from typing import Dict, List, Optional, Tuple, Union
class DistanceCalculator:
"""Calculate distances between geographic coordinates."""
# Earth radius in km
EARTH_RADIUS_KM = 6371.0
# Unit conversion factors (to km)
UNITS = {
'km': 1.0,
'miles': 0.621371,
'm': 1000.0,
'meters': 1000.0,
'nm': 0.539957, # nautical miles
'ft': 3280.84,
'feet': 3280.84,
}
def __init__(self, unit: str = "km", method: str = "haversine"):
"""
Initialize calculator.
Args:
unit: Output unit (km, miles, m, nm, ft)
method: Distance method (haversine, vincenty)
"""
self.unit = unit.lower()
self.method = method.lower()
if self.unit not in self.UNITS:
raise ValueError(f"Unknown unit: {unit}. Use: {list(self.UNITS.keys())}")
if self.method not in ('haversine', 'vincenty'):
raise ValueError(f"Unknown method: {method}. Use: haversine, vincenty")
def distance(
self,
point1: Tuple[float, float],
point2: Tuple[float, float]
) -> float:
"""
Calculate distance between two points.
Args:
point1: (lat, lon) first point
point2: (lat, lon) second point
Returns:
Distance in configured unit
"""
if self.method == 'haversine':
dist_km = self._haversine(point1, point2)
else:
dist_km = self._vincenty(point1, point2)
return self._convert_from_km(dist_km)
def _haversine(
self,
point1: Tuple[float, float],
point2: Tuple[float, float]
) -> float:
"""Calculate haversine distance in km."""
lat1, lon1 = math.radians(point1[0]), math.radians(point1[1])
lat2, lon2 = math.radians(point2[0]), math.radians(point2[1])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
c = 2 * math.asin(math.sqrt(a))
return self.EARTH_RADIUS_KM * c
def _vincenty(
self,
point1: Tuple[float, float],
point2: Tuple[float, float]
) -> float:
"""Calculate Vincenty distance in km (more accurate)."""
try:
from geopy.distance import geodesic
return geodesic(point1, point2).kilometers
except ImportError:
# Fallback to haversine
return self._haversine(point1, point2)
def _convert_from_km(self, km: float) -> float:
"""Convert km to configured unit."""
return km * self.UNITS[self.unit]
def convert(self, value: float, from_unit: str, to_unit: str) -> float:
"""
Convert between units.
Args:
value: Distance value
from_unit: Source unit
to_unit: Target unit
Returns:
Converted value
"""
# Convert to km first
km = value / self.UNITS[from_unit.lower()]
# Convert to target
return km * self.UNITS[to_unit.lower()]
def distance_with_details(
self,
point1: Tuple[float, float],
point2: Tuple[float, float]
) -> Dict:
"""
Get distance with full details.
Args:
point1: (lat, lon) first point
point2: (lat, lon) second point
Returns:
Dict with distance and metadata
"""
dist = self.distance(point1, point2)
return {
'distance': dist,
'unit': self.unit,
'from': {'lat': point1[0], 'lon': point1[1]},
'to': {'lat': point2[0], 'lon': point2[1]},
'method': self.method
}
def distance_matrix(
self,
points: List[Tuple[float, float]]
) -> List[List[float]]:
"""
Calculate all pairwise distances.
Args:
points: List of (lat, lon) tuples
Returns:
NxN distance matrix
"""
n = len(points)
matrix = [[0.0] * n for _ in range(n)]
for i in range(n):
for j in range(i + 1, n):
dist = self.distance(points[i], points[j])
matrix[i][j] = dist
matrix[j][i] = dist
return matrix
def distances_from_origin(
self,
origin: Tuple[float, float],
points: List[Tuple[float, float]]
) -> List[float]:
"""
Calculate distances from origin to all points.
Args:
origin: (lat, lon) origin point
points: List of destination points
Returns:
List of distances
"""
return [self.distance(origin, p) for p in points]
def find_nearest(
self,
origin: Tuple[float, float],
points: List[Union[Tuple[float, float], Dict]],
n: int = 1
) -> List[Dict]:
"""
Find nearest N points to origin.
Args:
origin: (lat, lon) origin point
points: List of points or dicts with lat/lon
n: Number of nearest to return
Returns:
List of nearest points with distances
"""
results = []
for p in points:
if isinstance(p, dict):
lat = p.get('lat') or p.get('latitude')
lon = p.get('lon') or p.get('lng') or p.get('longitude')
point = (lat, lon)
data = p
else:
point = p
data = {}
dist = self.distance(origin, point)
results.append({
'point': point,
'distance': dist,
'data': data
})
# Sort by distance
results.sort(key=lambda x: x['distance'])
return results[:n]
def find_within_radius(
self,
origin: Tuple[float, float],
points: List[Union[Tuple[float, float], Dict]],
radius: float
) -> List[Dict]:
"""
Find all points within radius of origin.
Args:
origin: (lat, lon) origin point
points: List of points
radius: Search radius in configured unit
Returns:
List of points within radius
"""
results = []
for p in points:
if isinstance(p, dict):
lat = p.get('lat') or p.get('latitude')
lon = p.get('lon') or p.get('lng') or p.get('longitude')
point = (lat, lon)
data = p
else:
point = p
data = {}
dist = self.distance(origin, point)
if dist <= radius:
results.append({
'point': point,
'distance': dist,
'data': data
})
results.sort(key=lambda x: x['distance'])
return results
def from_csv(
self,
filepath: str,
lat_col: str = 'lat',
lon_col: str = 'lon'
) -> List[Dict]:
"""
Load points from CSV.
Args:
filepath: Path to CSV
lat_col: Latitude column name
lon_col: Longitude column name
Returns:
List of point dicts
"""
points = []
with open(filepath, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
row['lat'] = float(row[lat_col])
row['lon'] = float(row[lon_col])
points.append(row)
return points
def matrix_to_csv(
self,
matrix: List[List[float]],
labels: List[str],
output: str
) -> str:
"""
Save distance matrix to CSV.
Args:
matrix: Distance matrix
labels: Point labels
output: Output file path
Returns:
Path to saved file
"""
with open(output, 'w', newline='') as f:
writer = csv.writer(f)
# Header
writer.writerow([''] + labels)
# Rows
for i, row in enumerate(matrix):
writer.writerow([labels[i]] + [f"{d:.2f}" for d in row])
return output
def parse_point(s: str) -> Tuple[float, float]:
"""Parse 'lat,lon' string to tuple."""
parts = s.split(',')
return (float(parts[0].strip()), float(parts[1].strip()))
def main():
"""CLI entry point."""
parser = argparse.ArgumentParser(description='Calculate geographic distances')
parser.add_argument('--from', dest='from_point', help='Origin point (lat,lon)')
parser.add_argument('--to', help='Destination point (lat,lon)')
parser.add_argument('--origin', help='Origin for search operations (lat,lon)')
parser.add_argument('--input', '-i', help='Input CSV file')
parser.add_argument('--lat', default='lat', help='Latitude column')
parser.add_argument('--lon', default='lon', help='Longitude column')
parser.add_argument('--nearest', type=int, help='Find N nearest points')
parser.add_argument('--radius', type=float, help='Search radius')
parser.add_argument('--matrix', action='store_true', help='Calculate distance matrix')
parser.add_argument('--output', '-o', help='Output file')
parser.add_argument('--unit', default='km', choices=['km', 'miles', 'm', 'nm', 'ft'])
parser.add_argument('--method', default='haversine', choices=['haversine', 'vincenty'])
args = parser.parse_args()
calc = DistanceCalculator(unit=args.unit, method=args.method)
# Point-to-point distance
if args.from_point and args.to:
p1 = parse_point(args.from_point)
p2 = parse_point(args.to)
result = calc.distance_with_details(p1, p2)
print(f"Distance: {result['distance']:.2f} {result['unit']}")
print(f"From: ({p1[0]}, {p1[1]})")
print(f"To: ({p2[0]}, {p2[1]})")
print(f"Method: {result['method']}")
# Search operations
elif args.origin and args.input:
origin = parse_point(args.origin)
points = calc.from_csv(args.input, args.lat, args.lon)
if args.nearest:
results = calc.find_nearest(origin, points, args.nearest)
print(f"Nearest {args.nearest} points:")
for i, r in enumerate(results, 1):
name = r['data'].get('name', f"Point {i}")
print(f" {i}. {name}: {r['distance']:.2f} {args.unit}")
elif args.radius:
results = calc.find_within_radius(origin, points, args.radius)
print(f"Points within {args.radius} {args.unit}:")
for r in results:
name = r['data'].get('name', str(r['point']))
print(f" {name}: {r['distance']:.2f} {args.unit}")
print(f"\nTotal: {len(results)} points")
# Distance matrix
elif args.input and args.matrix:
points_data = calc.from_csv(args.input, args.lat, args.lon)
points = [(p['lat'], p['lon']) for p in points_data]
labels = [p.get('name', f"P{i}") for i, p in enumerate(points_data)]
matrix = calc.distance_matrix(points)
if args.output:
calc.matrix_to_csv(matrix, labels, args.output)
print(f"Matrix saved to: {args.output}")
else:
# Print matrix
print(f"Distance Matrix ({args.unit}):")
print("\t" + "\t".join(labels))
for i, row in enumerate(matrix):
print(f"{labels[i]}\t" + "\t".join(f"{d:.1f}" for d in row))
else:
parser.print_help()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Geo Visualizer - Create interactive maps with markers, heatmaps, and more.
Features:
- Markers with custom icons and popups
- Heatmaps for density visualization
- Choropleth maps for regional data
- Routes and paths
- Marker clustering
"""
import argparse
import json
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union
class GeoVisualizer:
"""Create interactive maps using Folium."""
TILE_LAYERS = {
'openstreetmap': 'OpenStreetMap',
'cartodb positron': 'CartoDB positron',
'cartodb dark_matter': 'CartoDB dark_matter',
'stamen terrain': 'Stamen Terrain',
'stamen toner': 'Stamen Toner',
}
ICON_COLORS = [
'red', 'blue', 'green', 'purple', 'orange', 'darkred',
'lightred', 'beige', 'darkblue', 'darkgreen', 'cadetblue',
'darkpurple', 'white', 'pink', 'lightblue', 'lightgreen',
'gray', 'black', 'lightgray'
]
def __init__(
self,
center: Optional[Tuple[float, float]] = None,
zoom: int = 10,
tiles: str = "OpenStreetMap"
):
"""
Initialize map.
Args:
center: (lat, lon) center point
zoom: Initial zoom level (1-18)
tiles: Base map tiles
"""
self._folium = None
self._load_folium()
self.center = center or (0, 0)
self.zoom = zoom
self.tiles = tiles
self.map = None
self.data = None
self._markers = []
self._layers = []
self._init_map()
def _load_folium(self):
"""Load folium library."""
try:
import folium
self._folium = folium
except ImportError:
raise ImportError("folium required. Install with: pip install folium")
def _init_map(self):
"""Initialize the map object."""
self.map = self._folium.Map(
location=self.center,
zoom_start=self.zoom,
tiles=self.tiles
)
def from_csv(
self,
filepath: str,
lat_col: str,
lon_col: str,
**kwargs
) -> 'GeoVisualizer':
"""
Load data from CSV file.
Args:
filepath: Path to CSV
lat_col: Latitude column name
lon_col: Longitude column name
**kwargs: Additional pandas read_csv args
Returns:
self for chaining
"""
import pandas as pd
self.data = pd.read_csv(filepath, **kwargs)
self._lat_col = lat_col
self._lon_col = lon_col
# Auto-center on data
if self.center == (0, 0):
self.center = (
self.data[lat_col].mean(),
self.data[lon_col].mean()
)
self._init_map()
return self
def from_dataframe(
self,
df,
lat_col: str,
lon_col: str
) -> 'GeoVisualizer':
"""
Load data from pandas DataFrame.
Args:
df: DataFrame with location data
lat_col: Latitude column name
lon_col: Longitude column name
Returns:
self for chaining
"""
self.data = df.copy()
self._lat_col = lat_col
self._lon_col = lon_col
# Auto-center
if self.center == (0, 0):
self.center = (df[lat_col].mean(), df[lon_col].mean())
self._init_map()
return self
def from_geojson(self, filepath: str) -> 'GeoVisualizer':
"""
Load GeoJSON file.
Args:
filepath: Path to GeoJSON file
Returns:
self for chaining
"""
with open(filepath) as f:
self._geojson = json.load(f)
return self
def add_marker(
self,
lat: float,
lon: float,
popup: Optional[str] = None,
tooltip: Optional[str] = None,
icon: Optional[str] = None,
color: str = "blue"
) -> 'GeoVisualizer':
"""
Add a single marker.
Args:
lat: Latitude
lon: Longitude
popup: Popup content (HTML)
tooltip: Hover tooltip
icon: FontAwesome icon name (e.g., 'fa-coffee')
color: Marker color
Returns:
self for chaining
"""
marker_icon = None
if icon:
marker_icon = self._folium.Icon(color=color, icon=icon, prefix='fa')
else:
marker_icon = self._folium.Icon(color=color)
marker = self._folium.Marker(
location=[lat, lon],
popup=popup,
tooltip=tooltip,
icon=marker_icon
)
marker.add_to(self.map)
self._markers.append((lat, lon))
return self
def add_markers(
self,
locations: Optional[List[Dict]] = None,
name_col: Optional[str] = None,
popup_cols: Optional[List[str]] = None,
color: str = "blue"
) -> 'GeoVisualizer':
"""
Add multiple markers.
Args:
locations: List of dicts with lat/lon keys, or use loaded data
name_col: Column for tooltip
popup_cols: Columns to include in popup
color: Marker color
Returns:
self for chaining
"""
if locations is None and self.data is not None:
# Use loaded data
for _, row in self.data.iterrows():
lat = row[self._lat_col]
lon = row[self._lon_col]
# Build popup
popup = None
if popup_cols:
popup_lines = []
for col in popup_cols:
if col in row:
popup_lines.append(f"<b>{col}:</b> {row[col]}")
popup = "<br>".join(popup_lines)
tooltip = row.get(name_col) if name_col else None
self.add_marker(lat, lon, popup=popup, tooltip=tooltip, color=color)
else:
# Use provided locations
for loc in locations:
lat = loc.get('lat') or loc.get('latitude')
lon = loc.get('lon') or loc.get('lng') or loc.get('longitude')
popup = loc.get('popup') or loc.get('name')
tooltip = loc.get('tooltip') or loc.get('name')
self.add_marker(lat, lon, popup=popup, tooltip=tooltip, color=color)
return self
def cluster_markers(self, enabled: bool = True) -> 'GeoVisualizer':
"""
Enable marker clustering for dense data.
Args:
enabled: Whether to cluster
Returns:
self for chaining
"""
if enabled:
try:
from folium.plugins import MarkerCluster
self._marker_cluster = MarkerCluster().add_to(self.map)
except ImportError:
print("Warning: MarkerCluster not available")
return self
def add_heatmap(
self,
points: Optional[List[Tuple[float, float]]] = None,
weight_col: Optional[str] = None,
radius: int = 15,
blur: int = 10,
max_zoom: int = 12
) -> 'GeoVisualizer':
"""
Add heatmap layer.
Args:
points: List of (lat, lon) or (lat, lon, weight)
weight_col: Column for weights if using loaded data
radius: Heat point radius
blur: Blur amount
max_zoom: Max zoom for heatmap
Returns:
self for chaining
"""
try:
from folium.plugins import HeatMap
except ImportError:
raise ImportError("folium.plugins required for heatmap")
if points is None and self.data is not None:
if weight_col:
heat_data = [
[row[self._lat_col], row[self._lon_col], row[weight_col]]
for _, row in self.data.iterrows()
]
else:
heat_data = [
[row[self._lat_col], row[self._lon_col]]
for _, row in self.data.iterrows()
]
else:
heat_data = points
HeatMap(
heat_data,
radius=radius,
blur=blur,
max_zoom=max_zoom
).add_to(self.map)
return self
def add_choropleth(
self,
geojson: str,
data,
key_on: str,
value_col: str,
fill_color: str = "YlOrRd",
fill_opacity: float = 0.7,
line_opacity: float = 0.2,
legend_name: Optional[str] = None
) -> 'GeoVisualizer':
"""
Add choropleth layer.
Args:
geojson: Path to GeoJSON file
data: DataFrame with values
key_on: GeoJSON property to match
value_col: Data column for coloring
fill_color: Color scale
fill_opacity: Fill opacity
line_opacity: Border opacity
legend_name: Legend title
Returns:
self for chaining
"""
import pandas as pd
if isinstance(data, str):
data = pd.read_csv(data)
self._folium.Choropleth(
geo_data=geojson,
data=data,
columns=[data.columns[0], value_col],
key_on=key_on,
fill_color=fill_color,
fill_opacity=fill_opacity,
line_opacity=line_opacity,
legend_name=legend_name or value_col
).add_to(self.map)
return self
def add_route(
self,
points: List[Tuple[float, float]],
color: str = "blue",
weight: int = 3,
opacity: float = 1.0
) -> 'GeoVisualizer':
"""
Add route/path line.
Args:
points: List of (lat, lon) points
color: Line color
weight: Line width
opacity: Line opacity
Returns:
self for chaining
"""
self._folium.PolyLine(
points,
color=color,
weight=weight,
opacity=opacity
).add_to(self.map)
return self
def add_circle(
self,
lat: float,
lon: float,
radius_m: float,
color: str = "blue",
fill: bool = True,
fill_opacity: float = 0.3,
popup: Optional[str] = None
) -> 'GeoVisualizer':
"""
Add circle area.
Args:
lat: Center latitude
lon: Center longitude
radius_m: Radius in meters
color: Circle color
fill: Whether to fill
fill_opacity: Fill opacity
popup: Popup text
Returns:
self for chaining
"""
self._folium.Circle(
location=[lat, lon],
radius=radius_m,
color=color,
fill=fill,
fill_opacity=fill_opacity,
popup=popup
).add_to(self.map)
return self
def fit_bounds(self) -> 'GeoVisualizer':
"""
Fit map to show all markers.
Returns:
self for chaining
"""
if self._markers:
self.map.fit_bounds(self._markers)
elif self.data is not None:
bounds = [
[self.data[self._lat_col].min(), self.data[self._lon_col].min()],
[self.data[self._lat_col].max(), self.data[self._lon_col].max()]
]
self.map.fit_bounds(bounds)
return self
def add_layer_control(self) -> 'GeoVisualizer':
"""
Add layer control widget.
Returns:
self for chaining
"""
self._folium.LayerControl().add_to(self.map)
return self
def save(self, filepath: str) -> str:
"""
Save map to HTML file.
Args:
filepath: Output path
Returns:
Path to saved file
"""
self.map.save(filepath)
return filepath
def get_html(self) -> str:
"""Get map as HTML string."""
return self.map._repr_html_()
def main():
"""CLI entry point."""
parser = argparse.ArgumentParser(description='Create interactive maps')
parser.add_argument('--input', '-i', help='Input CSV file')
parser.add_argument('--lat', default='lat', help='Latitude column')
parser.add_argument('--lon', default='lon', help='Longitude column')
parser.add_argument('--output', '-o', default='map.html', help='Output HTML file')
parser.add_argument('--heatmap', action='store_true', help='Add heatmap layer')
parser.add_argument('--weight', help='Weight column for heatmap')
parser.add_argument('--cluster', action='store_true', help='Cluster markers')
parser.add_argument('--popup', nargs='+', help='Columns for popup')
parser.add_argument('--tooltip', help='Column for tooltip')
parser.add_argument('--tiles', default='OpenStreetMap', help='Base map tiles')
parser.add_argument('--geojson', help='GeoJSON file for choropleth')
parser.add_argument('--data', help='Data CSV for choropleth')
parser.add_argument('--key', help='GeoJSON key property')
parser.add_argument('--value', help='Value column for choropleth')
parser.add_argument('--color', default='blue', help='Marker color')
args = parser.parse_args()
viz = GeoVisualizer(tiles=args.tiles)
if args.geojson and args.data:
# Choropleth mode
viz.add_choropleth(
geojson=args.geojson,
data=args.data,
key_on=f"feature.properties.{args.key}",
value_col=args.value
)
elif args.input:
viz.from_csv(args.input, lat_col=args.lat, lon_col=args.lon)
if args.heatmap:
viz.add_heatmap(weight_col=args.weight)
else:
if args.cluster:
viz.cluster_markers(True)
viz.add_markers(
popup_cols=args.popup,
name_col=args.tooltip,
color=args.color
)
viz.fit_bounds()
else:
parser.print_help()
return
viz.save(args.output)
print(f"Map saved to: {args.output}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Geocoder - Convert between addresses and coordinates.
Features:
- Geocoding (address to coordinates)
- Reverse geocoding (coordinates to address)
- Batch processing
- Multiple providers
- CSV file operations
"""
import argparse
import csv
import time
from typing import Dict, List, Optional, Tuple
class Geocoder:
"""Geocoding and reverse geocoding operations."""
PROVIDERS = {
'nominatim': 'Nominatim',
'google': 'GoogleV3',
'bing': 'Bing',
'arcgis': 'ArcGIS',
}
def __init__(
self,
provider: str = "nominatim",
api_key: Optional[str] = None,
user_agent: str = "geocoder-skill"
):
"""
Initialize geocoder.
Args:
provider: Geocoding provider
api_key: API key for paid providers
user_agent: User agent for Nominatim
"""
self._geopy = None
self._load_geopy()
self.provider = provider.lower()
self.api_key = api_key
self.user_agent = user_agent
self._geocoder = self._init_geocoder()
def _load_geopy(self):
"""Load geopy library."""
try:
import geopy
self._geopy = geopy
except ImportError:
raise ImportError("geopy required. Install with: pip install geopy")
def _init_geocoder(self):
"""Initialize the geocoder for the selected provider."""
from geopy.geocoders import Nominatim, GoogleV3, Bing, ArcGIS
if self.provider == 'nominatim':
return Nominatim(user_agent=self.user_agent)
elif self.provider == 'google':
if not self.api_key:
raise ValueError("Google provider requires api_key")
return GoogleV3(api_key=self.api_key)
elif self.provider == 'bing':
if not self.api_key:
raise ValueError("Bing provider requires api_key")
return Bing(api_key=self.api_key)
elif self.provider == 'arcgis':
return ArcGIS()
else:
raise ValueError(f"Unknown provider: {self.provider}")
def geocode(self, address: str) -> Optional[Dict]:
"""
Convert address to coordinates.
Args:
address: Address string
Returns:
Dict with lat, lon, components or None
"""
try:
location = self._geocoder.geocode(address, addressdetails=True)
if location is None:
return None
result = {
'address': location.address,
'lat': location.latitude,
'lon': location.longitude,
'components': {},
'raw': location.raw
}
# Extract address components (Nominatim format)
if hasattr(location, 'raw') and 'address' in location.raw:
addr = location.raw['address']
result['components'] = {
'house_number': addr.get('house_number'),
'road': addr.get('road'),
'city': addr.get('city') or addr.get('town') or addr.get('village'),
'state': addr.get('state'),
'postcode': addr.get('postcode'),
'country': addr.get('country'),
}
return result
except Exception as e:
return {'error': str(e)}
def reverse(self, lat: float, lon: float) -> Optional[Dict]:
"""
Convert coordinates to address.
Args:
lat: Latitude
lon: Longitude
Returns:
Dict with address and components
"""
try:
location = self._geocoder.reverse(
f"{lat}, {lon}",
addressdetails=True
)
if location is None:
return None
result = {
'lat': lat,
'lon': lon,
'address': location.address,
'components': {},
'raw': location.raw
}
# Extract components
if hasattr(location, 'raw') and 'address' in location.raw:
addr = location.raw['address']
result['components'] = {
'house_number': addr.get('house_number'),
'road': addr.get('road'),
'city': addr.get('city') or addr.get('town') or addr.get('village'),
'state': addr.get('state'),
'postcode': addr.get('postcode'),
'country': addr.get('country'),
}
return result
except Exception as e:
return {'error': str(e)}
def batch_geocode(
self,
addresses: List[str],
delay: float = 1.0
) -> List[Optional[Dict]]:
"""
Geocode multiple addresses.
Args:
addresses: List of address strings
delay: Delay between requests (seconds)
Returns:
List of geocoding results
"""
results = []
for i, addr in enumerate(addresses):
result = self.geocode(addr)
results.append(result)
# Rate limiting
if i < len(addresses) - 1:
time.sleep(delay)
return results
def batch_reverse(
self,
coordinates: List[Tuple[float, float]],
delay: float = 1.0
) -> List[Optional[Dict]]:
"""
Reverse geocode multiple coordinates.
Args:
coordinates: List of (lat, lon) tuples
delay: Delay between requests
Returns:
List of reverse geocoding results
"""
results = []
for i, (lat, lon) in enumerate(coordinates):
result = self.reverse(lat, lon)
results.append(result)
if i < len(coordinates) - 1:
time.sleep(delay)
return results
def geocode_csv(
self,
input_path: str,
column: str,
output_path: str,
delay: float = 1.0
) -> Dict:
"""
Geocode addresses from CSV file.
Args:
input_path: Input CSV path
column: Address column name
output_path: Output CSV path
delay: Delay between requests
Returns:
Statistics dict
"""
import pandas as pd
df = pd.read_csv(input_path)
total = len(df)
success = 0
lats = []
lons = []
full_addresses = []
for i, row in df.iterrows():
address = row[column]
result = self.geocode(str(address))
if result and 'error' not in result:
lats.append(result['lat'])
lons.append(result['lon'])
full_addresses.append(result['address'])
success += 1
else:
lats.append(None)
lons.append(None)
full_addresses.append(None)
if i < total - 1:
time.sleep(delay)
# Progress
if (i + 1) % 10 == 0:
print(f"Processed {i + 1}/{total}")
df['geocoded_lat'] = lats
df['geocoded_lon'] = lons
df['geocoded_address'] = full_addresses
df.to_csv(output_path, index=False)
return {
'total': total,
'success': success,
'failed': total - success,
'output': output_path
}
def reverse_csv(
self,
input_path: str,
lat_col: str,
lon_col: str,
output_path: str,
delay: float = 1.0
) -> Dict:
"""
Reverse geocode coordinates from CSV.
Args:
input_path: Input CSV path
lat_col: Latitude column name
lon_col: Longitude column name
output_path: Output CSV path
delay: Delay between requests
Returns:
Statistics dict
"""
import pandas as pd
df = pd.read_csv(input_path)
total = len(df)
success = 0
addresses = []
for i, row in df.iterrows():
lat = row[lat_col]
lon = row[lon_col]
result = self.reverse(lat, lon)
if result and 'error' not in result:
addresses.append(result['address'])
success += 1
else:
addresses.append(None)
if i < total - 1:
time.sleep(delay)
if (i + 1) % 10 == 0:
print(f"Processed {i + 1}/{total}")
df['reverse_geocoded_address'] = addresses
df.to_csv(output_path, index=False)
return {
'total': total,
'success': success,
'failed': total - success,
'output': output_path
}
def main():
"""CLI entry point."""
parser = argparse.ArgumentParser(description='Geocoding operations')
parser.add_argument('--geocode', '-g', help='Address to geocode')
parser.add_argument('--reverse', '-r', help='Coordinates to reverse (lat,lon)')
parser.add_argument('--input', '-i', help='Input CSV file')
parser.add_argument('--column', '-c', help='Address column for geocoding')
parser.add_argument('--lat', default='lat', help='Latitude column')
parser.add_argument('--lon', default='lon', help='Longitude column')
parser.add_argument('--output', '-o', help='Output CSV file')
parser.add_argument('--reverse-batch', action='store_true', help='Batch reverse geocode')
parser.add_argument('--provider', default='nominatim', help='Geocoding provider')
parser.add_argument('--api-key', help='API key for provider')
parser.add_argument('--delay', type=float, default=1.0, help='Delay between requests')
args = parser.parse_args()
geo = Geocoder(provider=args.provider, api_key=args.api_key)
if args.geocode:
result = geo.geocode(args.geocode)
if result and 'error' not in result:
print(f"Address: {result['address']}")
print(f"Latitude: {result['lat']}")
print(f"Longitude: {result['lon']}")
if result['components']:
print("\nComponents:")
for k, v in result['components'].items():
if v:
print(f" {k}: {v}")
elif result:
print(f"Error: {result['error']}")
else:
print("Address not found")
elif args.reverse:
parts = args.reverse.split(',')
lat, lon = float(parts[0].strip()), float(parts[1].strip())
result = geo.reverse(lat, lon)
if result and 'error' not in result:
print(f"Address: {result['address']}")
if result['components']:
print("\nComponents:")
for k, v in result['components'].items():
if v:
print(f" {k}: {v}")
elif result:
print(f"Error: {result['error']}")
else:
print("Location not found")
elif args.input and args.output:
if args.reverse_batch:
stats = geo.reverse_csv(
args.input, args.lat, args.lon,
args.output, delay=args.delay
)
elif args.column:
stats = geo.geocode_csv(
args.input, args.column,
args.output, delay=args.delay
)
else:
parser.error("--column required for geocoding CSV")
return
print(f"\nCompleted:")
print(f" Total: {stats['total']}")
print(f" Success: {stats['success']}")
print(f" Failed: {stats['failed']}")
print(f" Output: {stats['output']}")
else:
parser.print_help()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""KML/GeoJSON Converter - Convert between geo formats."""
import argparse
import json
import os
from pathlib import Path
import geopandas as gpd
class GeoConverter:
"""Convert between KML and GeoJSON formats."""
def __init__(self):
self.gdf = None
def load_kml(self, filepath: str):
"""Load KML file."""
if not os.path.exists(filepath):
raise FileNotFoundError(f"KML file not found: {filepath}")
# Enable KML driver
gpd.io.file.fiona.drvsupport.supported_drivers['KML'] = 'rw'
self.gdf = gpd.read_file(filepath, driver='KML')
return self
def load_geojson(self, filepath: str):
"""Load GeoJSON file."""
if not os.path.exists(filepath):
raise FileNotFoundError(f"GeoJSON file not found: {filepath}")
self.gdf = gpd.read_file(filepath)
return self
def save_kml(self, output: str):
"""Save as KML file."""
if self.gdf is None:
raise ValueError("No data loaded")
os.makedirs(os.path.dirname(output) or '.', exist_ok=True)
# Enable KML driver
gpd.io.file.fiona.drvsupport.supported_drivers['KML'] = 'rw'
self.gdf.to_file(output, driver='KML')
return output
def save_geojson(self, output: str):
"""Save as GeoJSON file."""
if self.gdf is None:
raise ValueError("No data loaded")
os.makedirs(os.path.dirname(output) or '.', exist_ok=True)
self.gdf.to_file(output, driver='GeoJSON')
return output
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Convert between KML and GeoJSON formats')
parser.add_argument('input', help='Input file (KML or GeoJSON)')
parser.add_argument('--to', choices=['kml', 'geojson'], required=True, help='Output format')
parser.add_argument('--output', '-o', required=True, help='Output file path')
args = parser.parse_args()
converter = GeoConverter()
# Detect input format
input_ext = Path(args.input).suffix.lower()
print(f"Converting {args.input} to {args.to.upper()}...")
# Load input
if input_ext == '.kml':
converter.load_kml(args.input)
elif input_ext in ['.geojson', '.json']:
converter.load_geojson(args.input)
else:
print(f"Error: Unknown input format '{input_ext}'")
sys.exit(1)
# Save output
if args.to == 'kml':
converter.save_kml(args.output)
else:
converter.save_geojson(args.output)
print(f"✓ Converted to: {args.output}")
branca>=0.6.0
fiona>=1.9.0
folium>=0.14.0
geopandas>=0.14.0
geopy>=2.4.0
pandas>=2.0.0
shapely>=2.0.0
#!/usr/bin/env python3
"""Territory Mapper - Visualize territories on interactive maps."""
import argparse
import json
import os
from typing import List, Tuple, Dict, Any
import folium
import geopandas as gpd
from shapely.geometry import Polygon
import pandas as pd
class TerritoryMapper:
"""Create interactive territory maps."""
def __init__(self, center: Tuple[float, float] = None, zoom: int = 10):
"""Initialize map."""
self.center = center or (37.7749, -122.4194) # San Francisco default
self.zoom = zoom
self.map = folium.Map(location=self.center, zoom_start=self.zoom)
self.territories = []
def add_territory(self, name: str, coordinates: List[Tuple[float, float]],
color: str = 'blue', opacity: float = 0.5,
data: Dict[str, Any] = None):
"""
Add territory polygon to map.
Args:
name: Territory name
coordinates: List of (lat, lon) tuples defining polygon
color: Fill color
opacity: Fill opacity (0-1)
data: Additional data for tooltip
"""
# Create polygon
polygon = Polygon([(lon, lat) for lat, lon in coordinates])
# Create popup text
popup_text = f"<b>{name}</b><br>"
if data:
for key, value in data.items():
popup_text += f"{key}: {value}<br>"
# Add to map
folium.Polygon(
locations=coordinates,
color=color,
fill=True,
fillColor=color,
fillOpacity=opacity,
popup=popup_text,
tooltip=name
).add_to(self.map)
self.territories.append({
'name': name,
'geometry': polygon,
'color': color,
'data': data or {}
})
return self
def load_geojson(self, filepath: str, name_column: str = 'name',
color_column: str = None):
"""Load territories from GeoJSON file."""
if not os.path.exists(filepath):
raise FileNotFoundError(f"GeoJSON not found: {filepath}")
gdf = gpd.read_file(filepath)
# Color mapping
if color_column and color_column in gdf.columns:
# Generate colors based on unique values
unique_vals = gdf[color_column].unique()
colors = ['blue', 'red', 'green', 'purple', 'orange', 'darkred',
'lightred', 'beige', 'darkblue', 'darkgreen']
color_map = {val: colors[i % len(colors)]
for i, val in enumerate(unique_vals)}
else:
color_map = None
# Add each territory
for idx, row in gdf.iterrows():
name = row[name_column] if name_column in gdf.columns else f"Territory {idx}"
# Get coordinates
if row.geometry.geom_type == 'Polygon':
coords = [(y, x) for x, y in row.geometry.exterior.coords]
else:
continue # Skip non-polygons
# Determine color
if color_map:
color = color_map[row[color_column]]
else:
color = 'blue'
# Get additional data
data = {k: v for k, v in row.items()
if k not in ['geometry', name_column]}
self.add_territory(name, coords, color=color, data=data)
# Auto-fit bounds
self.map.fit_bounds(gdf.total_bounds[[1, 0, 3, 2]].reshape(2, 2).tolist())
return self
def save_html(self, output: str):
"""Save map as HTML."""
os.makedirs(os.path.dirname(output) or '.', exist_ok=True)
self.map.save(output)
return output
def add_markers(self, locations: List[Tuple[float, float, str]]):
"""Add markers to map (lat, lon, label)."""
for lat, lon, label in locations:
folium.Marker(
location=(lat, lon),
popup=label,
tooltip=label
).add_to(self.map)
return self
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Visualize territories on interactive maps')
parser.add_argument('--geojson', help='GeoJSON file with territories')
parser.add_argument('--name-column', default='name', help='Column for territory names')
parser.add_argument('--color-by', help='Column to color territories by')
parser.add_argument('--output', '-o', default='territories.html', help='Output HTML file')
parser.add_argument('--center', help='Map center as "lat,lon"')
parser.add_argument('--zoom', type=int, default=10, help='Initial zoom level')
args = parser.parse_args()
# Parse center
if args.center:
lat, lon = map(float, args.center.split(','))
center = (lat, lon)
else:
center = None
mapper = TerritoryMapper(center=center, zoom=args.zoom)
if args.geojson:
print(f"Loading territories from {args.geojson}...")
mapper.load_geojson(
args.geojson,
name_column=args.name_column,
color_column=args.color_by
)
print(f"Loaded {len(mapper.territories)} territories")
mapper.save_html(args.output)
print(f"✓ Map saved to: {args.output}")