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.