Skip to content
Go back

Self-Hosted HA Reverse Geocoding

By SumGuy 8 min read
Self-Hosted HA Reverse Geocoding

Wait — That’s Leaking What?

Your phone is in Home Assistant as a device_tracker. Every time it moves, HA stores the new coordinates and updates a friendly “Phone is at: 1234 Some Street, Some City.” That string is convenient. It’s also being generated by hitting a third-party geocoder somewhere — by default, the public OpenStreetMap Nominatim, but plenty of HA users also have integrations pointing at Google, Mapbox, or HERE.

So every location update your phone produces results in a GPS coordinate going out to someone else’s server. Multiply by the family. Multiply by the car tracker. Multiply by the dog’s GPS collar. Your home’s location history is being narrated, in real time, to whoever runs the geocoder you happen to be using.

Honestly, for a lot of people the answer is “I don’t care, it’s the OSM project.” Fair. But if you’ve gone to the trouble of self-hosting half your smart home, the geocoder is one of the easiest pieces to bring in-house. We’re talking 30 lines of YAML and one Docker container.

Full example: Home Assistant config and Compose stack at github.com/KingPin/sumguy-examples/tree/main/home-automation/home-assistant-nominatim-reverse-geocoding

What HA Does By Default (and Why That’s Annoying)

The OpenStreetMap reverse-geocoder integration in Home Assistant is convenient. It’s also pointed at the public nominatim.openstreetmap.org by default. A few things to know about that:

Other geocoder integrations are worse. Google’s reverse geocoding gives nice results but has all the obvious privacy implications and an API key that you’re paying for. HERE and Mapbox are similar.

The fix is dumb-simple in concept: run your own Nominatim, point HA at it. The result is faster, has no rate limits, leaks nothing, and works even when your internet connection has a bad day.

Prereq: A Local Nominatim

The rest of this post assumes you have a Nominatim instance running somewhere on your LAN — say at http://nominatim.lan:8080 or http://192.168.1.50:8080. The setup takes one Docker compose file and a regional PBF import.

If you don’t have Nominatim running yet, start with Nominatim: Self-Hosted Geocoding — that walks the Docker setup end-to-end. For HA reverse geocoding you can get away with just your country or state extract; you don’t need the planet.

A regional PBF for the country you live in is plenty. Honestly, for HA-only use, even a state or province extract works fine. The smaller the region, the faster the import, and the smaller the disk footprint.

The Wiring: Three Approaches

There are three reasonable ways to wire HA to a local Nominatim:

  1. The official OpenStreetMap integration with a custom server URL — depends on whether the integration exposes that field cleanly.
  2. A REST sensor that calls the local Nominatim reverse endpoint and stores the result.
  3. A template trigger sensor that fires on device_tracker state changes and looks up the address.

Approach 2 is the recommended path. It’s reliable, doesn’t depend on integration internals that can change between HA versions, and it’s easy to debug because the request is just HTTP that you can curl by hand.

We’ll cover 2 and 3 in detail.

Approach 1: REST Sensor Hitting Local Nominatim

The simplest pattern: a rest sensor that calls your local Nominatim’s reverse endpoint, templated against a device_tracker entity’s latitude and longitude attributes.

configuration.yaml
sensor:
- platform: rest
name: "Phone Location Address"
resource_template: >-
http://nominatim.lan:8080/reverse?lat={{ state_attr('device_tracker.my_phone', 'latitude') }}&lon={{ state_attr('device_tracker.my_phone', 'longitude') }}&format=json&zoom=18&addressdetails=1
value_template: "{{ value_json.display_name }}"
json_attributes:
- address
scan_interval: 300

What this does:

Now you can pull individual address fields out via template sensors:

configuration.yaml
template:
- sensor:
- name: "Phone City"
state: "{{ state_attr('sensor.phone_location_address', 'address').city }}"
- name: "Phone Street"
state: "{{ state_attr('sensor.phone_location_address', 'address').road }}"
- name: "Phone Country"
state: "{{ state_attr('sensor.phone_location_address', 'address').country }}"

That’s a working setup. Reload the YAML, restart HA, and the new sensors should populate within one polling interval.

Approach 2: Triggered Template Sensor

Polling every 5 minutes is wasteful — you only care about the address when the phone actually moves. A trigger-based template sensor is cleaner: it fires on device_tracker state changes and does the lookup only when needed.

configuration.yaml
template:
- trigger:
- platform: state
entity_id: device_tracker.my_phone
action:
- service: rest_command.nominatim_reverse
data:
lat: "{{ state_attr('device_tracker.my_phone', 'latitude') }}"
lon: "{{ state_attr('device_tracker.my_phone', 'longitude') }}"
response_variable: result
sensor:
- name: "Phone Address"
state: "{{ result['content']['display_name'] }}"
attributes:
city: "{{ result['content']['address'].city }}"
road: "{{ result['content']['address'].road }}"
postcode: "{{ result['content']['address'].postcode }}"

And the supporting rest_command:

configuration.yaml
rest_command:
nominatim_reverse:
url: "http://nominatim.lan:8080/reverse?lat={{ lat }}&lon={{ lon }}&format=json&zoom=18&addressdetails=1"
method: GET

The trigger pattern is more efficient and produces fresher data. The tradeoff is that it depends on the device_tracker actually emitting state changes — which most of them do, but if you have a tracker that updates lazily, the polling approach (approach 1) is more reliable.

Automation: Tell Me When the Car Gets Home

The real payoff is automations that use the reverse-geocoded address. A simple example: send a notification with the street name when a device enters or leaves the home zone.

automations.yaml
- alias: "Notify when car arrives home at night"
trigger:
- platform: state
entity_id: device_tracker.car
to: "home"
condition:
- condition: time
after: "22:00:00"
before: "06:00:00"
action:
- service: switch.turn_on
target:
entity_id: switch.porch_light
- service: notify.family
data:
title: "Welcome home"
message: >-
Car arrived from {{ state_attr('sensor.car_address', 'road') }}
at {{ now().strftime('%H:%M') }}.

You can get fancier — geofencing custom zones built from reverse-geocoded city values, “the car has been at the same address for 3 hours” alerts, “anyone is more than 50 km from home” notifications. The point is, once the address is in HA as a sensor, it composes with everything else HA does.

Privacy Wins You Get

What you actually buy with this setup:

The privacy framing isn’t paranoid — it’s just basic ergonomics. Your family’s location history shouldn’t have to traverse the public internet to give you a useful sensor value.

Things That Will Bite You

Bonus: Add a Map Card With Local Tiles

Once you’ve gone this far, the next obvious step is to stop loading map tiles from the public CDNs in your HA dashboard. That’s a bigger lift — you need a tile server (Martin or similar) and PostGIS — but it’s the same data source as Nominatim, and the operating cost is modest.

Want to go further and self-host the actual map tiles too? See The Full Self-Hosted Maps Stack — it walks through the Nominatim + PostGIS + Martin combo end-to-end.

Wrapping Up

The whole thing is one Docker container plus 30-ish lines of YAML in configuration.yaml. You get a faster, more private, more reliable address sensor for every tracked device in your home. No “free tier” to worry about, no API keys to rotate, no leak of your family’s coordinates to a third party every time someone walks to the mailbox.

Privacy-respecting smart home setups are usually built incrementally. This is one of the easy wins. Honestly, if you’ve already deployed a Nominatim instance for a side project, wiring HA to it is half an hour of work tops.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Boundary vs Teleport

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts