PostGIS Spatial Queries in PostgreSQL 18 – Practical OpenStreetMap Examples

In the previous articles of this PostgreSQL 18 series, we installed PostGIS, imported OpenStreetMap data for Bavaria, created GiST indexes, and finally visualized our data with Leaflet.

Having your data stored in PostGIS is only the first step. The real power comes from being able to ask spatial questions such as:

  • Which parks are within walking distance?
  • How far away is the nearest park?
  • Which park contains a specific landmark?
  • Which roads intersect a park?

These are the kinds of queries used by navigation systems, tourism websites, logistics platforms, and city information systems every day.

In this article, we’ll continue using the OpenStreetMap dataset for Bavaria that we imported in the first article. All examples work directly on the planet_osm_* tables created by osm2pgsql, so you can execute them without importing any additional data.

We’ll focus on the most commonly used PostGIS spatial functions:

  • ST_DWithin()
  • ST_Distance()
  • ST_Contains()
  • ST_Within()
  • ST_Intersects()

Let’s start with one of the most common GIS queries: finding nearby objects.


Finding Nearby Parks with ST_DWithin()

Imagine you’re building a tourism application for Munich.

A visitor standing on Marienplatz wants to find nearby parks within a radius of 10 kilometres.

First, we create a geometry representing Marienplatz. Since our OpenStreetMap data was imported with the default osm2pgsql settings, the geometries are stored in EPSG:3857. Therefore, we transform the WGS84 coordinates into the same coordinate reference system before performing the spatial query.

WITH munich AS (
    SELECT ST_Transform(
        ST_SetSRID(
            ST_MakePoint(11.57549,48.13743),
            4326
        ),
        3857
    ) AS geom
)
SELECT
    p.name,
    ROUND(ST_Distance(p.way,m.geom)) AS distance_m
FROM planet_osm_polygon p
CROSS JOIN munich m
WHERE p.leisure='park'
  AND p.name IS NOT NULL
  AND ST_DWithin(
        p.way,
        m.geom,
        10000
      )
ORDER BY distance_m
LIMIT 10;

The result should look similar to this:

           name           | distance_m
--------------------------+------------
 Hofgarten                |        835
 Maximiliansplatz         |        839
 Herzog-Wilhelm-Park      |        998
 Alter Botanischer Garten |       1175
 Dichtergarten            |       1203
 Nußbaumpark              |       1299
 Auf der Insel            |       1453
 Posthof                  |       1484
 Englischer Garten        |       1498
 Königsplatz              |       1604
(10 rows)

Your exact results may vary slightly depending on the OpenStreetMap data version you imported.

The function ST_DWithin() returns all geometries whose distance from the specified point is less than or equal to the given radius. Since our data is stored in EPSG:3857, the distance is measured in metres, making it ideal for proximity searches.

Typical use cases include finding:

  • nearby parks
  • restaurants
  • hospitals
  • charging stations
  • bus stops
  • hotels

Whenever you need to answer the question “What’s nearby?”, ST_DWithin() is usually the right function.


Why Not Use ST_Distance()?

Many developers start with a query like this:

SELECT
    name
FROM planet_osm_polygon
WHERE leisure='park'
AND ST_Distance(
        way,
        ST_Transform(
            ST_SetSRID(
                ST_MakePoint(11.57549,48.13743),
                4326
            ),
            3857
        )
    ) < 10000;

Although the query produces the correct result, it is not the most efficient solution.

ST_Distance() calculates the exact distance between the search point and every matching geometry before PostgreSQL can determine whether the row belongs in the result set.

For small tables this may not matter, but once your database contains hundreds of thousands or even millions of geometries, these calculations become expensive.

ST_DWithin() is specifically optimized for this use case. Together with a GiST index, PostgreSQL can first eliminate geometries that are clearly outside the search radius and calculate exact distances only for the remaining candidates.

If you followed the previous article on GiST indexes, you’ve already prepared your database for this optimization.

As a general rule:

  • Use ST_DWithin() to filter nearby objects.
  • Use ST_Distance() to display or sort by the exact distance.

Calculating Exact Distances

The most common pattern in PostGIS applications is to combine both functions.

ST_DWithin() limits the search to nearby geometries, while ST_Distance() calculates the exact distance for the remaining results.

WITH munich AS (
    SELECT ST_Transform(
        ST_SetSRID(
            ST_MakePoint(11.57549,48.13743),
            4326
        ),
        3857
    ) AS geom
)
SELECT
    p.name,
    ROUND(ST_Distance(p.way,m.geom)) AS distance_m
FROM planet_osm_polygon p
CROSS JOIN munich m
WHERE p.leisure='park'
  AND p.name IS NOT NULL
  AND ST_DWithin(
        p.way,
        m.geom,
        10000
      )
ORDER BY distance_m
LIMIT 10;

This approach has several advantages:

  • the GiST index reduces the number of candidate geometries,
  • the exact distance is calculated only for nearby parks,
  • and the results can easily be sorted from nearest to farthest.

This is the pattern you’ll encounter in many production GIS applications.


Which Park Contains the Chinesischer Turm?

Another common task is determining which polygon contains a particular object.

Suppose a visitor clicks on the Chinesischer Turm in your Leaflet application.

Rather than searching manually, PostGIS can determine which park contains this landmark.

SELECT
    p.name
FROM planet_osm_polygon p
JOIN planet_osm_point o
ON ST_Contains(
    p.way,
    o.way
)
WHERE p.leisure='park'
AND o.name='Chinesischer Turm';

The query returns:

Englischer Garten

The ST_Contains() function returns TRUE when one geometry completely contains another.

Typical applications include:

  • finding the city for a GPS coordinate,
  • determining the district containing a building,
  • assigning customers to sales territories,
  • locating landmarks inside parks,
  • or identifying administrative regions for an address.

In combination with interactive maps such as Leaflet, this function makes it easy to answer the question:

“What is located here?”

Instead of maintaining complex lookup tables, PostGIS performs the spatial relationship directly using the stored geometries.


Is the Olympiaturm Inside Olympiapark?

The opposite of ST_Contains() is ST_Within().

Instead of asking:

Which park contains this landmark?

we ask:

Is this landmark located inside the park?

Both functions describe the same spatial relationship, but from different perspectives.

The following example checks whether the Olympiaturm is located inside the Olympiapark.

SELECT
    ST_Within(tower.way, park.way)
FROM planet_osm_polygon tower
JOIN planet_osm_polygon park
ON park.name='Olympiapark'
WHERE tower.name='Olympiaturm';

The result is:

 st_within
-----------
 t

Whether you choose ST_Contains() or ST_Within() is mostly a matter of readability.

For example, these two questions are logically equivalent:

  • Does Olympiapark contain the Olympiaturm?
  • Is the Olympiaturm within Olympiapark?

Choose whichever makes your SQL easier to understand.

Typical applications include:

  • checking whether a building lies inside a municipality,
  • determining whether a GPS position is inside a geofence,
  • verifying that a customer belongs to a delivery zone,
  • validating whether an object is inside a protected area.

Which Roads Cross the Englischer Garten?

So far we’ve looked at relationships between points and polygons.

PostGIS can also compare entire geometries.

Suppose we want to know which roads intersect the Englischer Garten.

SELECT DISTINCT
    l.name
FROM planet_osm_line l
JOIN planet_osm_polygon p
ON ST_Intersects(
        l.way,
        p.way
)
WHERE p.name='Englischer Garten'
  AND l.highway IS NOT NULL
  AND l.name IS NOT NULL
ORDER BY l.name
LIMIT 20;
# Sample Output:
          name
------------------------
 Alte Lastenstraße
 Alte Parkstraße
 Am Englischen Garten
 Am Hirschanger
 Aumeisterbrücke
 Baumschulstraße
 Baumschulweg
 Blaue Brücke
 Burgfriedenweg
 Carl-August-Sckell-Weg
 Carl-Theodor-Brücke
 Carl-Theodor-Straße
 Dianabadbrücke
 Dianabadweg
 Diermayerweg
 Dietlindenstraße
 Effnerbrücke
 Effnerstraße
 Englischer Garten
 Entenfallbrücke
(20 rows)

Unlike ST_Contains(), ST_Intersects() only checks whether two geometries share at least one point.

That makes it one of the most useful functions for spatial joins.

Typical examples include:

  • roads crossing parks,
  • rivers crossing administrative boundaries,
  • buildings intersecting flood zones,
  • railway lines crossing municipalities,
  • utility lines crossing streets.

Spatial joins are one of the biggest strengths of PostGIS and often replace complicated application logic with a single SQL statement.


Combining PostGIS with Leaflet

In the previous article, we displayed our OpenStreetMap data using Leaflet.

By combining Leaflet with the spatial queries introduced here, you can build surprisingly powerful web applications with only a few SQL statements.

For example:

  • show parks near the user’s location,
  • display hospitals within 5 km,
  • highlight roads crossing a selected park,
  • determine which district a clicked point belongs to,
  • find cafés located inside a park.

A typical workflow looks like this:

  1. The user clicks somewhere on the map.
  2. Leaflet sends the coordinates to your backend.
  3. PostgreSQL executes one or more PostGIS queries.
  4. The matching geometries are returned as GeoJSON.
  5. Leaflet highlights the results on the map.

The combination of PostgreSQL, PostGIS and Leaflet provides an excellent foundation for interactive GIS applications without requiring proprietary software.


Summary

In this article, we used the OpenStreetMap dataset imported in the first part of this series to answer practical spatial questions around Munich.

We looked at the PostGIS functions you’ll use most often in day-to-day applications.

FunctionPurpose
ST_DWithin()Find nearby objects
ST_Distance()Calculate the exact distance
ST_Contains()Determine which polygon contains an object
ST_Within()Check whether an object lies inside another
ST_Intersects()Find overlapping geometries

Although PostGIS offers hundreds of functions, these five cover a large percentage of everyday GIS tasks.

Once you become comfortable with them, you’ll be able to build applications such as:

  • tourism and city guides,
  • route planners,
  • logistics platforms,
  • delivery services,
  • asset management systems,
  • emergency response applications,
  • interactive web maps.

Conclusion

With only a few SQL statements, PostGIS allows PostgreSQL to answer complex spatial questions that would otherwise require significant application logic.

Using the OpenStreetMap dataset imported earlier in this series, we’ve seen how to:

  • search for nearby parks,
  • calculate distances,
  • determine which park contains a landmark,
  • and identify roads intersecting a park.

These same techniques can be applied to virtually any type of geographic data, making PostGIS one of the most powerful spatial databases available today.

0
PostGIS gist

Speeding Up Spatial Queries: GiST Indexes and EXPLAIN ANALYZE with PostGIS on PostgreSQL 18

In the previous articles we built a production-ready PostGIS environment on PostgreSQL 18 and created an interactive Leaflet map that displays parks from OpenStreetMap data. While the application works, spatial queries can quickly become slow as the amount of data grows.

In this article we will investigate why certain spatial queries are slow, create our first GiST index, compare execution plans with EXPLAIN ANALYZE, and measure the real impact on map loading times.

Read More

0
PostGIS

Building a Production-Ready PostGIS Environment on PostgreSQL 18

PostGIS extends PostgreSQL with powerful spatial capabilities, enabling you to store, query, and analyze geographic data. It is the foundation for many Geographic Information Systems (GIS) used by municipalities, utility companies, logistics providers, and public authorities. In this article, we build a production-ready PostGIS environment that will serve as the basis for future articles covering performance tuning, indexing, maintenance, monitoring, backup strategies, and high availability.

Read More

0
PostGIS Leaflet

Building an Interactive Map with Leaflet and PostGIS on PostgreSQL 18

In the previous article, Building a Production-Ready PostGIS Environment on PostgreSQL 18, we installed PostGIS, created a spatial database, and imported OpenStreetMap data. While querying the database with SQL is useful, spatial data becomes much more valuable once it can be visualized on an interactive map.

In this article, we’ll build a simple web application using Leaflet, the most popular open-source JavaScript mapping library. The application will retrieve data directly from PostgreSQL/PostGIS and display it on an interactive OpenStreetMap layer. This environment will become the foundation for future articles covering performance tuning, GiST indexes, query optimization, monitoring, and database maintenance.


Architecture

Our application consists of four components:

Browser
    │
    ▼
Leaflet (JavaScript)
    │
    ▼
PHP Web Server
    │
    ▼
PostgreSQL 18 + PostGIS

The browser requests GeoJSON data from a PHP script, which executes a SQL query against PostgreSQL and returns the results in JSON format.


Prerequisites

This article assumes you already have:

  • PostgreSQL 18 installed
  • PostGIS enabled
  • OpenStreetMap data imported using osm2pgsql

If not, please read the previous article first.


Installing Apache and PHP

On RHEL 10 install Apache, PHP and the PostgreSQL driver and perform additional tasks.

# install packages
dnf -y install httpd php php-pgsql
# enable Apache
systemctl enable httpd --now
# open the firewall
firewall-cmd --add-service=http --permanent
firewall-cmd --reload

Preparing PostgreSQL

Create a dedicated database user for the web application.

# run as the postgres user
psql gisdemo <<EOF
-- create role
CREATE ROLE leaflet LOGIN PASSWORD 'changeme';
-- grant access
GRANT CONNECT ON DATABASE gisdemo TO leaflet;
GRANT USAGE ON SCHEMA public TO leaflet;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO leaflet;
\q
EOF


Creating the GeoJSON API

Create the following file as root

mkdir /var/www/html/api
vi /var/www/html/api/parks.php

with the following content:

<?php

$conn = pg_connect("
host=localhost
dbname=gisdemo
user=leaflet
password=changeme
");

$sql = "
SELECT
  json_build_object(
    'type','FeatureCollection',
    'features', json_agg(feature)
  )
FROM (
  SELECT
    json_build_object(
      'type','Feature',
      'geometry', ST_AsGeoJSON(ST_Transform(way, 4326))::json,
      'properties', json_build_object('name',name,'type',leisure)
    ) feature
  FROM planet_osm_polygon
  WHERE leisure='park'
    AND ST_DWithin(
          ST_Transform(way, 4326)::geography,
          ST_SetSRID(ST_MakePoint(11.576124, 48.137154), 4326)::geography,
          10000   -- 10 km radius
        )
) t;
";

$result = pg_query($conn, $sql);
header("Content-Type: application/json");
echo pg_fetch_result($result, 0, 0);
?>

To test the endpoint point a browser to:

http://lin6.fritz.box/api/parks.php

You should receive a GeoJSON FeatureCollection.


Creating the Leaflet Application

Create the following file as the root user

vi /var/www/html/index.html

with this content:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Leaflet + PostGIS</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
<style>
html, body { height:100%; margin:0; }
#map { height:100%; }
</style>
</head>
<body>
<div id="map"></div>
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script>
const map = L.map('map').setView([48.137154, 11.576124], 12);

L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '© OpenStreetMap contributors'
}).addTo(map);

fetch('api/parks.php')
    .then(response => response.json())
    .then(data => {
        console.log('Received GeoJSON:', data); // Check console
        const layer = L.geoJSON(data, {
            style: { color: 'green', weight: 2, fillOpacity: 0.5 },
            onEachFeature: function(feature, layer) {
                layer.bindPopup(feature.properties.name || 'Unnamed park');
            }
        }).addTo(map);

        // Zoom to the data extent
        const bounds = layer.getBounds();
        if (bounds.isValid()) {
            map.fitBounds(bounds);
            console.log('Zoomed to data bounds');
        } else {
            console.warn('No valid bounds from data');
        }
    })
    .catch(err => console.error('Fetch error:', err));
</script>
</body>
</html>

Viewing the Map

Point your browser to:

http://lin6.fritz.box/

You should now see:

  • OpenStreetMap
  • Munich
  • Parks imported from OpenStreetMap
  • Clickable polygons
  • Popups displaying park names

Unlike many tutorials, every object displayed comes directly from your OpenStreetMap import.


Understanding the Data Flow

The request follows a simple path.

Browser

↓

Leaflet

↓

parks.php

↓

PostgreSQL

↓

PostGIS

↓

ST_AsGeoJSON()

↓

GeoJSON

↓

Leaflet

The browser never communicates directly with PostgreSQL. Instead, PHP acts as a lightweight API that retrieves spatial data and converts it into GeoJSON.


Why GeoJSON?

GeoJSON has become the standard format for exchanging geographic information between databases and web applications.

Advantages include:

  • Native Leaflet support
  • Human-readable JSON
  • Generated directly by PostGIS
  • Supported by most GIS software
  • Lightweight and easy to debug

Exploring the Database

Let’s execute a few SQL statements to understand the imported dataset.

How many parks are available?

psql gisdemo <<EOF
SELECT count(*)
FROM planet_osm_polygon
WHERE leisure='park';
EOF
Sample Output (click to expand):
[postgres@lin6 ~]$ psql gisdemo <<EOF
SELECT count(*)
FROM planet_osm_polygon
WHERE leisure='park';
EOF
 count
-------
  6247
(1 row)

[postgres@lin6 ~]$

List some park names.

psql gisdemo <<EOF
SELECT
    name
FROM planet_osm_polygon
WHERE leisure='park'
ORDER BY name
LIMIT 20;
EOF
Sample Output (click to expand):
[postgres@lin6 ~]$ psql gisdemo <<EOF
SELECT
    name
FROM planet_osm_polygon
WHERE leisure='park'
ORDER BY name
LIMIT 20;
EOF
                          name
--------------------------------------------------------
 1A Hundegarten Kompetenzzentrum und Hundenasenparadies
 Abdurrahim-Özüdoğru-Park
 Aberdeenpark
 Abloner-Garten
 Achenpark
 Achenseeplatz
 Achenseeplatz
 Adamiwiese
 Adelheidpark
 AGFA-Park
 Ägidienplatz
 Ägidiuspark
 Agricolaplatz
 Aktiv Park
 Aktiv-Park
 Albert-Schweitzer-Park
 Albert-Schweitzer-Seniorenzentrum
 Albin-Lang-Stadtpark Landshut
 Alfons Halbig Platz
 Alfred-Dick-Park
(20 rows)

[postgres@lin6 ~]$

Calculate the area of the English Garden.

psql gisdemo <<EOF
SELECT 
  name,
  round(ST_Area(ST_Transform(way, 4326)::geography)) AS square_meters
FROM planet_osm_polygon
WHERE name = 'Englischer Garten'
ORDER BY square_meters DESC
LIMIT 1;
EOF
Output (click to expand):
[postgres@lin6 ~]$ psql gisdemo <<EOF
SELECT
  name,
  round(ST_Area(ST_Transform(way, 4326)::geography)) AS square_meters
FROM planet_osm_polygon
WHERE name = 'Englischer Garten'
ORDER BY square_meters DESC
LIMIT 1;
EOF
       name        | square_meters
-------------------+---------------
 Englischer Garten |       3931062
(1 row)

[postgres@lin6 ~]$

These examples demonstrate that PostGIS is much more than a storage engine—it provides advanced spatial analysis directly within PostgreSQL.


Preparing for Performance Tuning

The application is intentionally simple, but it already gives us a realistic workload that we can optimize.

In the upcoming articles we’ll use this same application to demonstrate:

  • GiST indexes
  • Bounding box queries
  • ST_DWithin
  • ST_Intersects
  • EXPLAIN ANALYZE
  • VACUUM
  • REINDEX
  • Autovacuum tuning
  • Streaming Replication
  • Backup and recovery
  • Monitoring

Every optimization will be measured by observing how quickly the map loads.


What’s Next?

In the next article we’ll investigate why some spatial queries become slow as the amount of OpenStreetMap data increases.

We’ll create our first GiST index, compare execution plans with EXPLAIN ANALYZE, and measure the impact on map loading times.


Conclusion

By combining PostgreSQL, PostGIS, OpenStreetMap, and Leaflet, we’ve built the foundation of a real-world GIS application. Rather than relying on artificial sample data, our application visualizes actual OpenStreetMap features stored in PostgreSQL, making it an ideal environment for exploring spatial queries, database administration, and performance tuning.

This architecture closely resembles those used by municipalities, utility companies, logistics providers, and many enterprise GIS platforms. Throughout the remainder of this series, we’ll continue extending and optimizing this application while learning how PostgreSQL and PostGIS handle large-scale spatial workloads.

0
PostgreSQL 18 Streaming Replication

Streaming Replication with PostgreSQL 18 on RHEL 10

PostgreSQL continues to be one of the most popular open-source database platforms for enterprise workloads, and the release of PostgreSQL 18 brings further improvements in performance, scalability and reliability. When deploying PostgreSQL in production environments, high availability and data protection are key considerations, making streaming replication one of the most important technologies to implement.

In this blog post, we will walk through the installation of PostgreSQL 18 on two Red Hat Enterprise Linux 10 virtual machines and configure a primary/standby architecture using PostgreSQL streaming replication. The guide covers the complete setup process, including package installation, database initialization, replication configuration, creation of a standby server and validation of the replication environment.

By the end of this tutorial, you will have a fully functional PostgreSQL replication setup that can serve as the foundation for high-availability and disaster-recovery solutions in enterprise environments.

Read More

0
Debugging PostgreSQL with VS Code

Debugging PostgreSQL with Visual Studio Code

Debugging a database system often requires looking beyond SQL statements and configuration parameters. When investigating complex issues, understanding what happens inside the PostgreSQL source code can provide valuable insights into query execution, process behavior and internal database mechanisms. For developers, contributors and advanced database administrators, the ability to step through PostgreSQL code can significantly simplify troubleshooting and performance analysis.

Read More

0