In [ ]:
import geopandas as gpd
from shapely.geometry import Point
from geopy.geocoders import Nominatim
from IPython.display import display
import requests
from openai import OpenAI
import folium
import json
import warnings
warnings.filterwarnings("ignore")
client = OpenAI(api_key="api-key")
In [ ]:
# Load zoning data
zoning_gdf = gpd.read_file("unincorporated_zoning.geojson").to_crs("EPSG:4326")
# Sanity Check!
zoning_gdf.head()
Out[ ]:
OBJECTID | Zoning | ZoningDescription | GroupLabel | UnitsPerAcre | MinimumLotAcres | MinimumLotSqFt | PlannedDistrict | NoticingBuffer | geometry | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 6745 | C-ARP-3 | Agriculture Residential Planned | Agriculture Residential Planned 3 Acres | 0.333 | 3.0 | 130680.0 | Residential | 600.0 | POLYGON ((-122.80931 38.05804, -122.80931 38.0... |
1 | 6746 | C-ARP-3 | Agriculture Residential Planned | Agriculture Residential Planned 3 Acres | 0.333 | 3.0 | 130680.0 | Residential | 600.0 | POLYGON ((-122.79959 38.07129, -122.79976 38.0... |
2 | 6747 | C-ARP-3 | Agriculture Residential Planned | Agriculture Residential Planned 3 Acres | 0.333 | 3.0 | 130680.0 | Residential | 600.0 | POLYGON ((-122.8054 38.07967, -122.8057 38.079... |
3 | 6748 | C-ARP-5 | Agriculture Residential Planned | Agriculture Residential Planned 5 Acres | 0.200 | 5.0 | 217800.0 | Residential | 600.0 | POLYGON ((-122.69337 37.91959, -122.69339 37.9... |
4 | 6749 | C-ARP-5 | Agriculture Residential Planned | Agriculture Residential Planned 5 Acres | 0.200 | 5.0 | 217800.0 | Residential | 600.0 | POLYGON ((-122.80583 38.06304, -122.80527 38.0... |
In [3]:
def geocode_address(address):
base_url = "https://geocoding.geo.census.gov/geocoder/locations/onelineaddress"
params = {
"address": address,
"benchmark": "Public_AR_Current",
"format": "json"
}
response = requests.get(base_url, params=params)
if response.status_code != 200:
return None
data = response.json()
matches = data.get("result", {}).get("addressMatches", [])
if not matches:
return None
coords = matches[0]["coordinates"]
return Point(coords["x"], coords["y"])
In [4]:
def lookup_zoning(address, buffer_meters=0.0001):
point_geom = geocode_address(address)
if not point_geom:
return None, None, "❌ Address could not be geocoded."
point_gdf = gpd.GeoDataFrame(geometry=[point_geom], crs="EPSG:4326")
# First attempt: raw point intersect
match = gpd.sjoin(zoning_gdf, point_gdf, how="inner", predicate="intersects")
if not match.empty:
return match, point_geom, None
# Fallback: buffered point
buffered_gdf = gpd.GeoDataFrame(geometry=[point_geom.buffer(buffer_meters)], crs="EPSG:4326")
match = gpd.sjoin(zoning_gdf, buffered_gdf, how="inner", predicate="intersects")
if not match.empty:
return match, point_geom, None
return None, point_geom, "❌ No zoning found even after buffering."
In [5]:
def show_result_map(point_geom, zone_gdf):
m = folium.Map(location=[point_geom.y, point_geom.x], zoom_start=17)
folium.Marker(location=[point_geom.y, point_geom.x], tooltip="Your Address").add_to(m)
if not zone_gdf.empty:
folium.GeoJson(zone_gdf).add_to(m)
display(m)
In [ ]:
def summarize_zoning(zone_gdf):
import json
# Drop geometry column and convert attributes to dict
info = zone_gdf.drop(columns="geometry").to_dict(orient="records")
# Build prompt
prompt = f"Summarize this zoning info in plain English:\n{json.dumps(info, indent=2)}"
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "user", "content": prompt}
]
)
return response.choices[0].message.content
In [7]:
def zoning_chatbot(address):
result, point_geom, error = lookup_zoning(address)
if error:
print(error)
return
show_result_map(point_geom, result)
print("✅ Zoning Summary:\n")
print(summarize_zoning(result))
In [8]:
zoning_gdf["geometry"] = zoning_gdf["geometry"].buffer(0)
In [9]:
zoning_chatbot("83 Wharf Rd, Bolinas, CA 94924")
Make this Notebook Trusted to load map: File -> Trust Notebook
✅ Zoning Summary: This zoning information indicates that the area is designated as Residential Agriculture with a zoning code of C-RA-B2. The zoning description specifies that it is for residential purposes on lots with a minimum of 10,000 square feet. The number of units allowed per acre is 4.356, with a minimum lot size of 0.229 acres. There is no planned district in place for this zoning and a noticing buffer of 300 feet.