1{ pkgs, lib, ... }:
2
3let
4 # Andorra - the smallest dataset in Europe (3.1 MB)
5 osmData = pkgs.fetchurl {
6 url = "https://web.archive.org/web/20250430211212/https://download.geofabrik.de/europe/andorra-latest.osm.pbf";
7 hash = "sha256-Ey+ipTOFUm80rxBteirPW5N4KxmUsg/pCE58E/2rcyE=";
8 };
9in
10{
11 name = "nominatim";
12 meta = {
13 maintainers = with lib.teams; [
14 geospatial
15 ngi
16 ];
17 };
18
19 nodes = {
20 # nominatim - self contained host
21 nominatim =
22 { config, pkgs, ... }:
23 {
24 # Nominatim
25 services.nominatim = {
26 enable = true;
27 hostName = "nominatim";
28 settings = {
29 NOMINATIM_IMPORT_STYLE = "admin";
30 };
31 ui = {
32 config = ''
33 Nominatim_Config.Page_Title='Test Nominatim instance';
34 Nominatim_Config.Nominatim_API_Endpoint='https://localhost/';
35 '';
36 };
37 };
38
39 # Disable SSL
40 services.nginx.virtualHosts.nominatim = {
41 forceSSL = false;
42 enableACME = false;
43 };
44
45 # Database
46 services.postgresql = {
47 enableTCPIP = true;
48 authentication = lib.mkForce ''
49 local all all trust
50 host all all 0.0.0.0/0 md5
51 host all all ::0/0 md5
52 '';
53 };
54 systemd.services.postgresql-setup.postStart = ''
55 psql --command "ALTER ROLE \"nominatim-api\" WITH PASSWORD 'password';"
56 '';
57 networking.firewall.allowedTCPPorts = [ config.services.postgresql.settings.port ];
58 };
59
60 # api - web API only
61 api =
62 { config, pkgs, ... }:
63 {
64 # Database password
65 system.activationScripts = {
66 passwordFile.text = with config.services.nominatim.database; ''
67 mkdir -p /run/secrets
68 echo "${host}:${toString port}:${dbname}:${apiUser}:password" \
69 > /run/secrets/pgpass
70 chown nominatim-api:nominatim-api /run/secrets/pgpass
71 chmod 0600 /run/secrets/pgpass
72 '';
73 };
74
75 # Nominatim
76 services.nominatim = {
77 enable = true;
78 hostName = "nominatim";
79 settings = {
80 NOMINATIM_LOG_DB = "yes";
81 };
82 database = {
83 host = "nominatim";
84 passwordFile = "/run/secrets/pgpass";
85 extraConnectionParams = "application_name=nominatim;connect_timeout=2";
86 };
87 };
88
89 # Disable SSL
90 services.nginx.virtualHosts.nominatim = {
91 forceSSL = false;
92 enableACME = false;
93 };
94 };
95 };
96
97 testScript = ''
98 # Test nominatim host
99 nominatim.start()
100 nominatim.wait_for_unit("nominatim.service")
101
102 # Import OSM data
103 nominatim.succeed("""
104 cd /tmp
105 sudo -u nominatim \
106 NOMINATIM_DATABASE_WEBUSER=nominatim-api \
107 NOMINATIM_IMPORT_STYLE=admin \
108 nominatim import --continue import-from-file --osm-file ${osmData}
109 """)
110 nominatim.succeed("systemctl restart nominatim.service")
111
112 # Test CLI
113 nominatim.succeed("sudo -u nominatim-api nominatim search --query Andorra")
114
115 # Test web API
116 nominatim.succeed("curl 'http://localhost/status' | grep OK")
117
118 nominatim.succeed("""
119 curl "http://localhost/search?q=Andorra&format=geojson" | grep "Andorra"
120 curl "http://localhost/reverse?lat=42.5407167&lon=1.5732033&format=geojson"
121 """)
122
123 # Test UI
124 nominatim.succeed("""
125 curl "http://localhost/ui/search.html" \
126 | grep "<title>Nominatim Demo</title>"
127 """)
128
129
130 # Test api host
131 api.start()
132 api.wait_for_unit("nominatim.service")
133
134 # Test web API
135 api.succeed("""
136 curl "http://localhost/search?q=Andorra&format=geojson" | grep "Andorra"
137 curl "http://localhost/reverse?lat=42.5407167&lon=1.5732033&format=geojson"
138 """)
139
140
141 # Test format rewrites
142 # Redirect / to search
143 nominatim.succeed("""
144 curl --verbose "http://localhost" 2>&1 \
145 | grep "Location: http://localhost/ui/search.html"
146 """)
147
148 # Return text by default
149 nominatim.succeed("""
150 curl --verbose "http://localhost/status" 2>&1 \
151 | grep "Content-Type: text/plain"
152 """)
153
154 # Return JSON by default
155 nominatim.succeed("""
156 curl --verbose "http://localhost/search?q=Andorra" 2>&1 \
157 | grep "Content-Type: application/json"
158 """)
159
160 # Return XML by default
161 nominatim.succeed("""
162 curl --verbose "http://localhost/lookup" 2>&1 \
163 | grep "Content-Type: text/xml"
164
165 curl --verbose "http://localhost/reverse?lat=0&lon=0" 2>&1 \
166 | grep "Content-Type: text/xml"
167 """)
168
169 # Redirect explicitly requested HTML format
170 nominatim.succeed("""
171 curl --verbose "http://localhost/search?format=html" 2>&1 \
172 | grep "Location: http://localhost/ui/search.html"
173
174 curl --verbose "http://localhost/reverse?format=html" 2>&1 \
175 | grep "Location: http://localhost/ui/reverse.html"
176 """)
177
178 # Return explicitly requested JSON format
179 nominatim.succeed("""
180 curl --verbose "http://localhost/search?format=json" 2>&1 \
181 | grep "Content-Type: application/json"
182
183 curl --verbose "http://localhost/reverse?format=json" 2>&1 \
184 | grep "Content-Type: application/json"
185 """)
186 '';
187}