]> git.openstreetmap.org Git - nominatim.git/blob - lib-sql/tokenizer/legacy_tokenizer.sql
introduce and use analyzer for postcodes
[nominatim.git] / lib-sql / tokenizer / legacy_tokenizer.sql
1 -- SPDX-License-Identifier: GPL-2.0-only
2 --
3 -- This file is part of Nominatim. (https://nominatim.org)
4 --
5 -- Copyright (C) 2022 by the Nominatim developer community.
6 -- For a full list of authors see the git log.
7
8 -- Get tokens used for searching the given place.
9 --
10 -- These are the tokens that will be saved in the search_name table.
11 CREATE OR REPLACE FUNCTION token_get_name_search_tokens(info JSONB)
12   RETURNS INTEGER[]
13 AS $$
14   SELECT (info->>'names')::INTEGER[]
15 $$ LANGUAGE SQL IMMUTABLE STRICT;
16
17
18 -- Get tokens for matching the place name against others.
19 --
20 -- This should usually be restricted to full name tokens.
21 CREATE OR REPLACE FUNCTION token_get_name_match_tokens(info JSONB)
22   RETURNS INTEGER[]
23 AS $$
24   SELECT (info->>'names')::INTEGER[]
25 $$ LANGUAGE SQL IMMUTABLE STRICT;
26
27
28 -- Return the housenumber tokens applicable for the place.
29 CREATE OR REPLACE FUNCTION token_get_housenumber_search_tokens(info JSONB)
30   RETURNS INTEGER[]
31 AS $$
32   SELECT (info->>'hnr_tokens')::INTEGER[]
33 $$ LANGUAGE SQL IMMUTABLE STRICT;
34
35
36 -- Return the housenumber in the form that it can be matched during search.
37 CREATE OR REPLACE FUNCTION token_normalized_housenumber(info JSONB)
38   RETURNS TEXT
39 AS $$
40   SELECT info->>'hnr';
41 $$ LANGUAGE SQL IMMUTABLE STRICT;
42
43
44 CREATE OR REPLACE FUNCTION token_has_addr_street(info JSONB)
45   RETURNS BOOLEAN
46 AS $$
47   SELECT info->>'street' is not null;
48 $$ LANGUAGE SQL IMMUTABLE;
49
50
51 CREATE OR REPLACE FUNCTION token_has_addr_place(info JSONB)
52   RETURNS BOOLEAN
53 AS $$
54   SELECT info->>'place_match' is not null;
55 $$ LANGUAGE SQL IMMUTABLE;
56
57
58 CREATE OR REPLACE FUNCTION token_matches_street(info JSONB, street_tokens INTEGER[])
59   RETURNS BOOLEAN
60 AS $$
61   SELECT (info->>'street')::INTEGER[] && street_tokens
62 $$ LANGUAGE SQL IMMUTABLE STRICT;
63
64
65 CREATE OR REPLACE FUNCTION token_matches_place(info JSONB, place_tokens INTEGER[])
66   RETURNS BOOLEAN
67 AS $$
68   SELECT (info->>'place_match')::INTEGER[] && place_tokens
69 $$ LANGUAGE SQL IMMUTABLE STRICT;
70
71
72 CREATE OR REPLACE FUNCTION token_addr_place_search_tokens(info JSONB)
73   RETURNS INTEGER[]
74 AS $$
75   SELECT (info->>'place_search')::INTEGER[]
76 $$ LANGUAGE SQL IMMUTABLE STRICT;
77
78
79 CREATE OR REPLACE FUNCTION token_get_address_keys(info JSONB)
80   RETURNS SETOF TEXT
81 AS $$
82   SELECT * FROM jsonb_object_keys(info->'addr');
83 $$ LANGUAGE SQL IMMUTABLE STRICT;
84
85
86 CREATE OR REPLACE FUNCTION token_get_address_search_tokens(info JSONB, key TEXT)
87   RETURNS INTEGER[]
88 AS $$
89   SELECT (info->'addr'->key->>0)::INTEGER[];
90 $$ LANGUAGE SQL IMMUTABLE STRICT;
91
92
93 CREATE OR REPLACE FUNCTION token_matches_address(info JSONB, key TEXT, tokens INTEGER[])
94   RETURNS BOOLEAN
95 AS $$
96   SELECT (info->'addr'->key->>1)::INTEGER[] && tokens;
97 $$ LANGUAGE SQL IMMUTABLE STRICT;
98
99
100 CREATE OR REPLACE FUNCTION token_normalized_postcode(postcode TEXT)
101   RETURNS TEXT
102 AS $$
103   SELECT CASE WHEN postcode SIMILAR TO '%(,|;)%' THEN NULL ELSE upper(trim(postcode))END;
104 $$ LANGUAGE SQL IMMUTABLE STRICT;
105
106
107 -- Return token info that should be saved permanently in the database.
108 CREATE OR REPLACE FUNCTION token_strip_info(info JSONB)
109   RETURNS JSONB
110 AS $$
111   SELECT NULL::JSONB;
112 $$ LANGUAGE SQL IMMUTABLE STRICT;
113
114 --------------- private functions ----------------------------------------------
115
116 -- Functions for term normalisation and access to the 'word' table.
117
118 CREATE OR REPLACE FUNCTION transliteration(text) RETURNS text
119   AS '{{ modulepath }}/nominatim.so', 'transliteration'
120 LANGUAGE c IMMUTABLE STRICT;
121
122
123 CREATE OR REPLACE FUNCTION gettokenstring(text) RETURNS text
124   AS '{{ modulepath }}/nominatim.so', 'gettokenstring'
125 LANGUAGE c IMMUTABLE STRICT;
126
127
128 CREATE OR REPLACE FUNCTION make_standard_name(name TEXT) RETURNS TEXT
129   AS $$
130 DECLARE
131   o TEXT;
132 BEGIN
133   o := public.gettokenstring(public.transliteration(name));
134   RETURN trim(substr(o,1,length(o)));
135 END;
136 $$
137 LANGUAGE plpgsql IMMUTABLE;
138
139 -- returns NULL if the word is too common
140 CREATE OR REPLACE FUNCTION getorcreate_word_id(lookup_word TEXT) 
141   RETURNS INTEGER
142   AS $$
143 DECLARE
144   lookup_token TEXT;
145   return_word_id INTEGER;
146   count INTEGER;
147 BEGIN
148   lookup_token := trim(lookup_word);
149   SELECT min(word_id), max(search_name_count) FROM word
150     WHERE word_token = lookup_token and class is null and type is null
151     INTO return_word_id, count;
152   IF return_word_id IS NULL THEN
153     return_word_id := nextval('seq_word');
154     INSERT INTO word VALUES (return_word_id, lookup_token, null, null, null, null, 0);
155   ELSE
156     IF count > {{ max_word_freq }} THEN
157       return_word_id := NULL;
158     END IF;
159   END IF;
160   RETURN return_word_id;
161 END;
162 $$
163 LANGUAGE plpgsql;
164
165
166 -- Create housenumber tokens from an OSM addr:housenumber.
167 -- The housnumber is split at comma and semicolon as necessary.
168 -- The function returns the normalized form of the housenumber suitable
169 -- for comparison.
170 CREATE OR REPLACE FUNCTION create_housenumbers(housenumbers TEXT[],
171                                                OUT tokens TEXT,
172                                                OUT normtext TEXT)
173   AS $$
174 BEGIN
175   SELECT array_to_string(array_agg(trans), ';'), array_agg(tid)::TEXT
176     INTO normtext, tokens
177     FROM (SELECT lookup_word as trans, getorcreate_housenumber_id(lookup_word) as tid
178           FROM (SELECT make_standard_name(h) as lookup_word
179                 FROM unnest(housenumbers) h) x) y;
180 END;
181 $$ LANGUAGE plpgsql STABLE STRICT;
182
183
184 CREATE OR REPLACE FUNCTION getorcreate_housenumber_id(lookup_word TEXT)
185   RETURNS INTEGER
186   AS $$
187 DECLARE
188   lookup_token TEXT;
189   return_word_id INTEGER;
190 BEGIN
191   lookup_token := ' ' || trim(lookup_word);
192   SELECT min(word_id) FROM word
193     WHERE word_token = lookup_token and class='place' and type='house'
194     INTO return_word_id;
195   IF return_word_id IS NULL THEN
196     return_word_id := nextval('seq_word');
197     INSERT INTO word VALUES (return_word_id, lookup_token, null,
198                              'place', 'house', null, 0);
199   END IF;
200   RETURN return_word_id;
201 END;
202 $$
203 LANGUAGE plpgsql;
204
205
206 CREATE OR REPLACE FUNCTION create_postcode_id(postcode TEXT)
207   RETURNS BOOLEAN
208   AS $$
209 DECLARE
210   r RECORD;
211   lookup_token TEXT;
212   return_word_id INTEGER;
213 BEGIN
214   lookup_token := ' ' || make_standard_name(postcode);
215   FOR r IN
216     SELECT word_id FROM word
217     WHERE word_token = lookup_token and word = postcode
218           and class='place' and type='postcode'
219   LOOP
220     RETURN false;
221   END LOOP;
222
223   INSERT INTO word VALUES (nextval('seq_word'), lookup_token, postcode,
224                            'place', 'postcode', null, 0);
225   RETURN true;
226 END;
227 $$
228 LANGUAGE plpgsql;
229
230
231 CREATE OR REPLACE FUNCTION getorcreate_name_id(lookup_word TEXT, src_word TEXT)
232   RETURNS INTEGER
233   AS $$
234 DECLARE
235   lookup_token TEXT;
236   nospace_lookup_token TEXT;
237   return_word_id INTEGER;
238 BEGIN
239   lookup_token := ' '||trim(lookup_word);
240   SELECT min(word_id) FROM word
241   WHERE word_token = lookup_token and class is null and type is null
242   INTO return_word_id;
243   IF return_word_id IS NULL THEN
244     return_word_id := nextval('seq_word');
245     INSERT INTO word VALUES (return_word_id, lookup_token, src_word,
246                              null, null, null, 0);
247   END IF;
248   RETURN return_word_id;
249 END;
250 $$
251 LANGUAGE plpgsql;
252
253
254 -- Normalize a string and lookup its word ids (partial words).
255 CREATE OR REPLACE FUNCTION addr_ids_from_name(lookup_word TEXT)
256   RETURNS INTEGER[]
257   AS $$
258 DECLARE
259   words TEXT[];
260   id INTEGER;
261   return_word_id INTEGER[];
262   word_ids INTEGER[];
263   j INTEGER;
264 BEGIN
265   words := string_to_array(make_standard_name(lookup_word), ' ');
266   IF array_upper(words, 1) IS NOT NULL THEN
267     FOR j IN 1..array_upper(words, 1) LOOP
268       IF (words[j] != '') THEN
269         SELECT array_agg(word_id) INTO word_ids
270           FROM word
271          WHERE word_token = words[j] and class is null and type is null;
272
273         IF word_ids IS NULL THEN
274           id := nextval('seq_word');
275           INSERT INTO word VALUES (id, words[j], null, null, null, null, 0);
276           return_word_id := return_word_id || id;
277         ELSE
278           return_word_id := array_merge(return_word_id, word_ids);
279         END IF;
280       END IF;
281     END LOOP;
282   END IF;
283
284   RETURN return_word_id;
285 END;
286 $$
287 LANGUAGE plpgsql;
288
289
290 -- Normalize a string and look up its name ids (full words).
291 CREATE OR REPLACE FUNCTION word_ids_from_name(lookup_word TEXT)
292   RETURNS INTEGER[]
293   AS $$
294 DECLARE
295   lookup_token TEXT;
296   return_word_ids INTEGER[];
297 BEGIN
298   lookup_token := ' '|| make_standard_name(lookup_word);
299   SELECT array_agg(word_id) FROM word
300     WHERE word_token = lookup_token and class is null and type is null
301     INTO return_word_ids;
302   RETURN return_word_ids;
303 END;
304 $$
305 LANGUAGE plpgsql STABLE STRICT;
306
307
308 CREATE OR REPLACE FUNCTION make_keywords(src HSTORE)
309   RETURNS INTEGER[]
310   AS $$
311 DECLARE
312   result INTEGER[];
313   s TEXT;
314   w INTEGER;
315   words TEXT[];
316   value TEXT;
317   j INTEGER;
318 BEGIN
319   result := '{}'::INTEGER[];
320
321   FOR value IN SELECT unnest(regexp_split_to_array(svals(src), E'[,;]')) LOOP
322     -- full name
323     s := make_standard_name(value);
324     w := getorcreate_name_id(s, value);
325
326     IF not(ARRAY[w] <@ result) THEN
327       result := result || w;
328     END IF;
329
330     -- partial single-word terms
331     words := string_to_array(s, ' ');
332     IF array_upper(words, 1) IS NOT NULL THEN
333       FOR j IN 1..array_upper(words, 1) LOOP
334         IF (words[j] != '') THEN
335           w = getorcreate_word_id(words[j]);
336           IF w IS NOT NULL AND NOT (ARRAY[w] <@ result) THEN
337             result := result || w;
338           END IF;
339         END IF;
340       END LOOP;
341     END IF;
342
343     -- consider parts before an opening braket a full word as well
344     words := regexp_split_to_array(value, E'[(]');
345     IF array_upper(words, 1) > 1 THEN
346       s := make_standard_name(words[1]);
347       IF s != '' THEN
348         w := getorcreate_name_id(s, words[1]);
349         IF w IS NOT NULL AND NOT (ARRAY[w] <@ result) THEN
350           result := result || w;
351         END IF;
352       END IF;
353     END IF;
354
355     s := regexp_replace(value, '市$', '');
356     IF s != value THEN
357       s := make_standard_name(s);
358       IF s != '' THEN
359         w := getorcreate_name_id(s, value);
360         IF NOT (ARRAY[w] <@ result) THEN
361           result := result || w;
362         END IF;
363       END IF;
364     END IF;
365
366   END LOOP;
367
368   RETURN result;
369 END;
370 $$
371 LANGUAGE plpgsql;
372
373
374 CREATE OR REPLACE FUNCTION precompute_words(src TEXT)
375   RETURNS INTEGER
376   AS $$
377 DECLARE
378   s TEXT;
379   w INTEGER;
380   words TEXT[];
381   i INTEGER;
382   j INTEGER;
383 BEGIN
384   s := make_standard_name(src);
385   w := getorcreate_name_id(s, src);
386
387   w := getorcreate_word_id(s);
388
389   words := string_to_array(s, ' ');
390   IF array_upper(words, 1) IS NOT NULL THEN
391     FOR j IN 1..array_upper(words, 1) LOOP
392       IF (words[j] != '') THEN
393         w := getorcreate_word_id(words[j]);
394       END IF;
395     END LOOP;
396   END IF;
397
398   words := regexp_split_to_array(src, E'[,;()]');
399   IF array_upper(words, 1) != 1 THEN
400     FOR j IN 1..array_upper(words, 1) LOOP
401       s := make_standard_name(words[j]);
402       IF s != '' THEN
403         w := getorcreate_word_id(s);
404       END IF;
405     END LOOP;
406   END IF;
407
408   s := regexp_replace(src, '市$', '');
409   IF s != src THEN
410     s := make_standard_name(s);
411     IF s != '' THEN
412       w := getorcreate_name_id(s, src);
413     END IF;
414   END IF;
415
416   RETURN 1;
417 END;
418 $$
419 LANGUAGE plpgsql;