-- The location column is NULL until geocoded CREATE TABLE restaurant_inspections ( restaurant_name VARCHAR2(200), address VARCHAR2(300), city VARCHAR2(100), state VARCHAR2(2), zip_code VARCHAR2(10), violation_count NUMBER(3), location MDSYS.SDO_GEOMETRY -- ← spatial column ); -- Initial state after INSERT: SELECT restaurant_name, address, location FROM restaurant_inspections; -- RESTAURANT_NAME ADDRESS LOCATION -- Au Cheval 800 W Randolph St (null) -- Green Street 112 N Green St (null) -- Alinea 1723 N Halsted St (null)
The location column (MDSYS.SDO_GEOMETRY) is NULL until we geocode. This tutorial walks every step from raw address text to interactive heat map — using real Oracle ADB coordinates throughout.
-- SDO_GCDR is an Oracle PL/SQL PACKAGE. -- It is NOT a REST API — you call it in SQL. -- On ADB it uses Oracle ELOC (HERE Maps) -- internally. No API key needed from you. -- Single address — JSON form (ADB 23ai): SELECT SDO_GCDR.ELOC_GEOCODE_AS_GEOM( JSON_OBJECT('address' VALUE '800 W Randolph St, Chicago IL') ) AS location FROM dual; -- Returns: SDO_GEOMETRY(2001, 4326, -- SDO_POINT_TYPE(-87.64662, 41.88427, NULL), -- NULL, NULL) -- Update all rows in one statement: UPDATE restaurant_inspections SET location = SDO_GCDR.ELOC_GEOCODE_AS_GEOM( address, city, state, zip_code, 'US' ) WHERE location IS NULL; -- 20 rows updated. All geocoded.
SDO_GCDR.ELOC_GEOCODE_AS_GEOM() is called in a SQL UPDATE statement — no HTTP request from your code, no API key to manage. The result is stored directly as SDO_GEOMETRY. Oracle calls HERE Maps internally as part of the ELOC service included with ADB.
-- TABLE(SDO_UTIL.GETVERTICES(geom)) is a -- lateral join. For a Point: 1 row. -- For a Polygon: N rows (one per vertex). -- Single address: SELECT v.x AS longitude, v.y AS latitude FROM ( SELECT SDO_GCDR.ELOC_GEOCODE_AS_GEOM( JSON_OBJECT('address' VALUE '800 W Randolph St, Chicago IL') ) AS geom FROM dual ) g, TABLE(SDO_UTIL.GETVERTICES(g.geom)) v; -- Result: -87.64662 41.88427 -- From the full table: SELECT r.restaurant_name, v.x AS longitude, v.y AS latitude, r.violation_count FROM restaurant_inspections r, TABLE(SDO_UTIL.GETVERTICES(r.location)) v WHERE r.location IS NOT NULL;
| Restaurant | v.x (lng) | v.y (lat) | Viol. |
|---|
TABLE(SDO_UTIL.GETVERTICES(geom)) works identically for Points (1 row), LineStrings, and Polygons (N rows). The coordinate swap — [v.y, v.x] for Leaflet — is the single most common mistake when connecting Oracle Spatial to web maps.
-- ORDS is built into every ADB. -- One SQL block exposes any query as REST: -- AutoREST: same approach used by the CDC dashboard. -- One call per object — no module/template/handler needed. -- CORS is handled automatically by Oracle ADB. -- Run once as ADMIN or the schema user: BEGIN ORDS.ENABLE_OBJECT( p_enabled => TRUE, p_schema => 'SDO', p_object => 'RESTAURANT_GEOJSON_V', p_object_type => 'VIEW', p_object_alias => 'violations' ); COMMIT; END; -- That's it! The URL is now live: -- /ords/sdo/violations/ -- Returns: { "items": [...], "hasMore": false, "count": 20 } -- JavaScript fetch — no mode, no proxy, no headers needed: const res = await fetch('https://...oraclecloudapps.com/ords/sdo/violations/'); const rows = (await res.json()).items;
Click to make a real HTTP GET to your Oracle ADB instance in OCI Phoenix. Data comes directly from SDO.RESTAURANT_GEOJSON_V.
No separate server, no config file, no deployment pipeline. ORDS.DEFINE_HANDLER() turns any SQL query into a REST endpoint. The response is standard JSON wrapped in {"items":[...]}. CORS is enabled with ORDS.SET_MODULE_ORIGINS_ALLOWED('heatmap','*') — already done in your ADB.
// Include in <head>: <link rel="stylesheet" href="leaflet.css"/> <script src="leaflet.js"></script> <style> #map { height: 500px; } </style> <div id="map"></div> // Initialize map — [lat, lng] ← lat FIRST const map = L.map('map') .setView([41.9003, -87.6534], 13); // [41.9003, -87.6534] = [v.y, v.x] from Oracle // Tile layer — free, no API key L.tileLayer( 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: '© CartoDB', maxZoom: 19 } ).addTo(map); // {z}/{x}/{y} = zoom/col/row tiles // Leaflet calculates these automatically
Leaflet takes [latitude, longitude] — opposite to Oracle's SDO_POINT_TYPE(longitude, latitude). So setView([v.y, v.x]). The map div must have an explicit CSS height — a block element defaults to height:0 and Leaflet renders nothing.
// rows = (await fetch(ORDS_URL)).json().items // Each row: { latitude, longitude, violation_count } function color(v) { return v>=7 ? '#dc2626' // red : v>=5 ? '#d97706' // amber : v>=3 ? '#059669' // green : '#2563eb'; // blue } rows.forEach(r => { L.circleMarker( [r.latitude, r.longitude], // [v.y, v.x]! { radius: 4 + r.violation_count * 1.2, fillColor: color(r.violation_count), color: 'white', weight: 1.5, fillOpacity: 0.85 } ) .bindPopup(`<b>${r.restaurant_name}</b> <br/>${r.violation_count} violations`) .addTo(map); });
L.circleMarker() radius is in pixels — stays constant as you zoom, ideal for data viz. L.circle() radius is in metres — scales with zoom, ideal for showing a spatial query radius like SDO_WITHIN_DISTANCE. Notice the ORDS response already has latitude and longitude as column names from the view's GETVERTICES extraction.
-- Oracle computes 0.0–1.0 intensity in the VIEW. -- No JavaScript math needed. -- Inside restaurant_geojson_v: ROUND( r.violation_count / NULLIF(MAX(r.violation_count) OVER(), 0), 4 ) AS heat_intensity -- OVER() = window = entire result set -- MAX(violation_count) OVER() = 9 -- Results from your ADB: -- Au Cheval 9 → 1.0000 -- Green Street 8 → 0.8889 -- Kumas Corner 7 → 0.7778 -- Blackbird 6 → 0.6667 -- ... -- Alinea 0 → 0.0000 -- JavaScript just reads the column: rows.map(r => [r.latitude, r.longitude, r.heat_intensity])
| Restaurant | Violations | Intensity | Bar |
|---|
The analytic expression MAX(violation_count) OVER () runs in one SQL pass across the full result set. The normalised value arrives in every ORDS row — JavaScript reads r.heat_intensity directly with zero extra computation.
const ORDS = 'https://bk8uwrvkgqzvi2h-vbjson.adb'+ '.us-phoenix-1.oraclecloudapps.com'+ '/ords/sdo/heatmap/violations'; async function buildMap() { // 1. Fetch live from Oracle ORDS const rows = (await (await fetch(ORDS)).json()).items; // 2. Leaflet map const map = L.map('map').setView([41.9,-87.65],13); L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {attribution:'© CartoDB'}).addTo(map); // 3. Heat layer — Oracle intensity direct L.heatLayer( rows.map(r=>[r.latitude, r.longitude, r.heat_intensity]), { radius:35, blur:20, max:1.0, gradient:{0:'#3b82f6',0.5:'#f59e0b',1:'#ef4444'} } ).addTo(map); // 4. Circle markers rows.forEach(r => L.circleMarker([r.latitude, r.longitude], {radius:4+r.violation_count*1.1, fillColor:r.violation_count>6?'#dc2626':'#2563eb', color:'white',weight:1.5,fillOpacity:0.85}) .bindPopup(`<b>${r.restaurant_name}</b><br/>${r.violation_count} violations`) .addTo(map) ); } buildMap();
Oracle geocoded the addresses → stored SDO_GEOMETRY → GETVERTICES extracted coordinates → the view normalised intensity with MAX() OVER() → ORDS served JSON → JavaScript mapped [r.latitude, r.longitude, r.heat_intensity] into L.heatLayer(). Every hard part happened in Oracle SQL.
Since 2023, Oracle Autonomous Database 23ai includes a built-in geocoder. One SQL call converts a plain text address into a precise geographic coordinate stored as MDSYS.SDO_GEOMETRY — no external API, no key management, no extra cost. This post shows the complete pipeline from that single SQL call to a live interactive heat map.
Before 2023, geocoding in Oracle required a separately licensed on-premises geocoder or an external API call from your application. Since ADB 23ai, it is a single SQL function call that runs entirely inside the database. Oracle handles the call to HERE Maps internally — your code never touches an HTTP endpoint.
The result is an SDO_GEOMETRY point with SRID 4326 (WGS84 — the same coordinate system used by GPS, Google Maps, and Leaflet). SDO_UTIL.GETVERTICES() is the lateral join that unpacks it: v.x = longitude, v.y = latitude. It works identically for Points, Lines, and Polygons — making it the preferred extraction method.
You call it in a SQL UPDATE or SELECT statement. There is no HTTP call from your application code, no API key to rotate, and no per-call billing beyond your ADB subscription.
Once your addresses are geocoded into SDO_GEOMETRY, you have two good options for visualisation:
SDO_UTIL.TO_GEOJSON() to convert SDO_GEOMETRY to standard GeoJSON. Export via ORDS. Render in any web application. Full control over styling, interactivity, and layout.This demo uses Leaflet.js with the leaflet-heat plugin — completely free, no API key, works offline. The heat map intensity is computed directly in Oracle SQL using an analytic window function (MAX(violation_count) OVER ()) so JavaScript receives pre-normalised 0.0–1.0 values and maps them directly to colours.
Oracle REST Data Services (ORDS) is built into every ADB instance. Five lines of SQL turns any table or view into a live REST endpoint that any browser can fetch — including full CORS support out of the box.
The p_auto_rest_auth => FALSE flag makes the endpoint public — no login needed. The ORDS response wraps rows in {"items":[...]} and includes full pagination. The HTML file fetches it with a single await fetch(url) call.
Set p_auto_rest_auth => TRUE, then go to Database Actions → REST → Security → OAuth Clients. Create a client, issue a client_credentials grant, and pass the token as Authorization: Bearer <token> in your fetch call. No external identity provider required — Oracle ADB handles it natively.
The complete demo is a single self-contained HTML file. It embeds all the JavaScript, CSS, and Oracle data. It fetches live data from ORDS. It requires no build step, no npm, no framework. Here are three ways to serve it:
python -m http.server 8502Everything in this demo is running live on Oracle ADB 23ai. The full stack, and the cost of each component:
The complete setup SQL and this HTML file are available as a single GitHub repository. Clone, update the ADB hostname, run the SQL script, serve the HTML — fully working spatial heat map in under 15 minutes on any Oracle ADB 23ai instance.
This entire demo — the database setup, the geocoding, the ORDS endpoint, the HTML heat map, all three tabs, the SQL reference script — was built through a conversation with Claude AI connected directly to the Oracle ADB via SQLcl MCP. AI ran every SQL statement live, read the results, corrected errors, and iterated until everything worked.
The combination of Oracle ADB 23ai spatial capabilities + AI code generation + ORDS Auto-REST means a developer can go from a table of addresses to a fully deployed interactive heat map in a single working session — without reading a single page of documentation.