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:
- The public Nominatim is rate-limited to roughly 1 request per second per source. If you have a busy household with multiple device_trackers, you’ll hit limits.
- The fair use policy explicitly says it’s for occasional use, not continuous polling. People do it anyway, and OSM occasionally bans IP ranges as a result.
- Every coordinate goes out over the public internet to a server you don’t control.
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:
- The official OpenStreetMap integration with a custom server URL — depends on whether the integration exposes that field cleanly.
- A REST sensor that calls the local Nominatim reverse endpoint and stores the result.
- A template trigger sensor that fires on
device_trackerstate 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.
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: 300What this does:
- Polls every 5 minutes (
scan_interval: 300) - Builds the URL from the current
latitudeandlongitudeattributes ofdevice_tracker.my_phone - The
valueof the sensor becomes the human-readabledisplay_name - The full structured
addressobject (road, suburb, city, postcode, country) is exposed as an attribute
Now you can pull individual address fields out via template sensors:
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.
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:
rest_command: nominatim_reverse: url: "http://nominatim.lan:8080/reverse?lat={{ lat }}&lon={{ lon }}&format=json&zoom=18&addressdetails=1" method: GETThe 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.
- 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:
- No coordinates leave the LAN. Every reverse geocode is local DNS, local HTTP, local Postgres.
- No rate limits. Geocode every state change of every tracker in the house if you want.
- No API keys to rotate. No “your free tier expires next month” emails.
- No third-party logs of your family’s movements. Whatever’s in your local Nominatim logs is yours.
- Faster lookups. 5–20ms on the LAN vs 100–300ms to a public API.
- Works without internet. If your ISP goes down, your smart home keeps narrating addresses correctly.
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
- HA template sensors are picky about URL escaping. If a coordinate has weird characters, escape it. The
urlencodefilter is your friend. - Don’t poll every 30 seconds. Geocoding the same street 100 times an hour is silly and pointlessly hammers your Nominatim. Trigger-based or 5-minute polling is plenty.
- If Nominatim’s down, your sensors go unavailable. Wrap automations in availability checks (
{% if states('sensor.phone_address') not in ['unknown', 'unavailable'] %}). - Don’t expose Nominatim to the public internet. Keep it on the LAN or behind your existing reverse proxy with auth.
- The
zoomparameter matters.zoom=18returns building-level detail,zoom=10city-level,zoom=3country-level. Pick what you actually want. - Watch for stale
latitude/longitudeattributes. Some device_trackers cache their coords. If reverse geocoding seems off, check whether HA’s coords match reality.
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.
Related posts
- Nominatim: Self-Hosted Geocoding — Docker install walkthrough
- Nominatim vs Photon vs Pelias — picking the right engine
- Nominatim Hardware Sizing — RAM and disk math
- Full Self-Hosted Maps Stack — add tile serving on top