]> git.openstreetmap.org Git - nominatim.git/blob - lib/SearchDescription.php
convert getGroupedSearches to SearchDescription class
[nominatim.git] / lib / SearchDescription.php
1 <?php
2
3 namespace Nominatim;
4
5 /**
6  * Operators describing special searches.
7  */
8 abstract final class Operator
9 {
10     /// No operator selected.
11     const NONE = 0;
12     /// Search for POI of the given type.
13     const TYPE = 1;
14     /// Search for POIs near the given place.
15     const NEAR = 2;
16     /// Search for POIS in the given place.
17     const IN = 3;
18     /// Search for POIS named as given.
19     const NAME = 4;
20     /// Search for postcodes.
21     const POSTCODE = 5;
22 }
23
24 /**
25  * Description of a single interpretation of a search query.
26  */
27 class SearchDescription
28 {
29     /// Ranking how well the description fits the query.
30     private $iSearchRank = 0;
31     /// Country code of country the result must belong to.
32     private $sCountryCode = '';
33     /// List of word ids making up the name of the object.
34     private $aName = array();
35     /// List of word ids making up the address of the object.
36     private $aAddress = array();
37     /// Subset of word ids of full words making up the address.
38     private $aFullNameAddress = array();
39     /// List of word ids that appear in the name but should be ignored.
40     private $aNameNonSearch = array();
41     /// List of word ids that appear in the address but should be ignored.
42     private $aAddressNonSearch = array();
43     /// Kind of search for special searches, see Nominatim::Operator.
44     private $iOperator = Operator::NONE;
45     /// Class of special feature to search for.
46     private $sClass = '';
47     /// Type of special feature to search for.
48     private $sType = '';
49     /// Housenumber of the object.
50     private $sHouseNumber = '';
51     /// Postcode for the object.
52     private $sPostcode = '';
53     /// Geographic search area.
54     private $oNearPoint = false;
55
56     // Temporary values used while creating the search description.
57
58     /// Index of phrase currently processed
59     private $iNamePhrase = -1;
60
61     public function getRank()
62     {
63         return $this->iSearchRank;
64     }
65
66     public function addToRank($iAddRank)
67     {
68         $this->iSearchRank += $iAddRank;
69         return $this->iSearchRank;
70     }
71
72     public function getPostCode()
73     {
74         return $this->sPostcode;
75     }
76
77     /**
78      * Set the geographic search radius.
79      */
80     public function setNear(&$oNearPoint)
81     {
82         $this->oNearPoint = $oNearPoint;
83     }
84
85     public function setPoiSearch($iOperator, $sClass, $sType)
86     {
87         $this->iOperator = $iOperator;
88         $this->sClass = $sClass;
89         $this->sType = $sType;
90     }
91
92     /**
93      * Check if name or address for the search are specified.
94      */
95     public function isNamedSearch()
96     {
97         return sizeof($this->aName) > 0 || sizeof($this->aAddress) > 0;
98     }
99
100     /**
101      * Check if only a country is requested.
102      */
103     public function isCountrySearch()
104     {
105         return $this->sCountryCode && sizeof($this->aName) == 0
106                && !$this->iOperator && !$this->oNear;
107     }
108
109     /**
110      * Check if a search near a geographic location is requested.
111      */
112     public function isNearSearch()
113     {
114         return (bool) $this->oNear;
115     }
116
117     public function isPoiSearch()
118     {
119         return (bool) $this->sClass;
120     }
121
122     public function looksLikeFullAddress()
123     {
124         return sizeof($this->aName)
125                && (sizeof($this->aAddress || $this->sCountryCode))
126                && preg_match('/[0-9]+/', $this->sHouseNumber);
127     }
128
129     public function isOperator($iType)
130     {
131         return $this->iOperator == $iType;
132     }
133
134     public function hasHouseNumber()
135     {
136         return (bool) $this->sHouseNumber;
137     }
138
139     private function poiTable()
140     {
141         return 'place_classtype_'.$this->sClass.'_'.$this->sType;
142     }
143
144     public function countryCodeSQL($sVar, $sCountryList)
145     {
146         if ($this->sCountryCode) {
147             return $sVar.' = \''.$this->sCountryCode."'";
148         }
149         if ($sCountryList) {
150             return $sVar.' in ('.$this->sCountryCode.')';
151         }
152
153         return '';
154     }
155
156     public function hasOperator()
157     {
158         return $this->iOperator != Operator::NONE;
159     }
160
161     /**
162      * Extract special terms from the query, amend the search
163      * and return the shortended query.
164      *
165      * Only the first special term found will be used but all will
166      * be removed from the query.
167      */
168     public function extractKeyValuePairs($sQuery)
169     {
170         // Search for terms of kind [<key>=<value>].
171         preg_match_all(
172             '/\\[([\\w_]*)=([\\w_]*)\\]/',
173             $sQuery,
174             $aSpecialTermsRaw,
175             PREG_SET_ORDER
176         );
177
178         foreach ($aSpecialTermsRaw as $aTerm) {
179             $sQuery = str_replace($aTerm[0], ' ', $sQuery);
180             if (!$this->hasOperator()) {
181                 $this->setPoiSearch(Operator::TYPE, $aTerm[1], $aTerm[2]);
182             }
183         }
184
185         return $sQuery;
186     }
187
188     public function isValidSearch(&$aCountryCodes)
189     {
190         if (!sizeof($this->aName)) {
191             if ($this->sHouseNumber) {
192                 return false;
193             }
194         }
195         if ($aCountryCodes
196             && $this->sCounrtyCode
197             && !in_array($this->sCountryCode, $aCountryCodes)
198         ) {
199             return false;
200         }
201
202         return true;
203     }
204
205     /////////// Search building functions
206
207     public function extendWithFullTerm($aSearchTerm, $bWordInQuery, $bHasPartial, $sPhraseType, $bFirstToken, $bFirstPhrase, $bLastToken, &$iGlobalRank)
208     {
209         $aNewSearches = array();
210
211         if (($sPhraseType == '' || $sPhraseType == 'country')
212             && !empty($aSearchTerm['country_code'])
213             && $aSearchTerm['country_code'] != '0'
214         ) {
215             if (!$this->sCountryCode) {
216                 $oSearch = clone $this;
217                 $oSearch->iSearchRank++;
218                 $oSearch->sCountryCode = $aSearchTerm['country_code'];
219                 // Country is almost always at the end of the string
220                 // - increase score for finding it anywhere else (optimisation)
221                 if (!$bLastToken) {
222                     $oSearch->iSearchRank += 5;
223                 }
224                 $aNewSearches[] = $oSearch;
225
226                 // If it is at the beginning, we can be almost sure that
227                 // the terms are in the wrong order. Increase score for all searches.
228                 if ($bFirstToken) {
229                     $iGlobalRank++;
230                 }
231             }
232         } elseif (($sPhraseType == '' || $sPhraseType == 'postalcode')
233                   && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'postcode'
234         ) {
235             // We need to try the case where the postal code is the primary element
236             // (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode)
237             // so try both.
238             if (!$this->sPostcode && $bWordInQuery) {
239                 // If we have structured search or this is the first term,
240                 // make the postcode the primary search element.
241                 if ($this->iOperator == Operator::NONE
242                     && ($sPhraseType == 'postalcode' || $bFirstToken)
243                 ) {
244                     $oSearch = clone $this;
245                     $oSearch->iSearchRank++;
246                     $oSearch->iOperator = Operator::POSTCODE;
247                     $oSearch->aAddress = array_merge($this->aAddress, $this->aName);
248                     $oSearch->aName =
249                         array($aSearchTerm['word_id'] => $aSearchTerm['word']);
250                     $aNewSearches[] = $oSearch;
251                 }
252
253                 // If we have a structured search or this is not the first term,
254                 // add the postcode as an addendum.
255                 if ($this->iOperator != Operator::POSTCODE
256                     && ($sPhraseType == 'postalcode' || sizeof($this->aName))
257                 ) {
258                     $oSearch = clone $this;
259                     $oSearch->iSearchRank++;
260                     $oSearch->sPostcode = $aSearchTerm['word'];
261                     $aNewSearches[] = $oSearch;
262                 }
263             }
264         } elseif (($sPhraseType == '' || $sPhraseType == 'street')
265                  && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house'
266         ) {
267             if (!$this->sHouseNumber && $this->iOperator != Operator::POSTCODE) {
268                 $oSearch = clone $this;
269                 $oSearch->iSearchRank++;
270                 $oSearch->sHouseNumber = trim($aSearchTerm['word_token']);
271                 // sanity check: if the housenumber is not mainly made
272                 // up of numbers, add a penalty
273                 if (preg_match_all("/[^0-9]/", $oSearch->sHouseNumber, $aMatches) > 2) {
274                     $oSearch->iSearchRank++;
275                 }
276                 // also must not appear in the middle of the address
277                 if (sizeof($this->aAddress) || sizeof($this->aAddressNonSearch)) {
278                     $oSearch->iSearchRank++;
279                 }
280                 $aNewSearches[] = $oSearch;
281             }
282         } elseif ($sPhraseType == ''
283                   && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null
284         ) {
285             // require a normalized exact match of the term
286             // if we have the normalizer version of the query
287             // available
288             if ($this->iOperator == Operator::NONE
289                 && (isset($aSearchTerm['word']) && $aSearchTerm['word'])
290                 && $bWordInQuery
291             ) {
292                 $oSearch = clone this;
293                 $oSearch->iSearchRank++;
294
295                 $iOp = Operator::NEAR; // near == in for the moment
296                 if ($aSearchTerm['operator'] == '') {
297                     if (sizeof($this->aName)) {
298                         $iOp = Operator::NAME;
299                     }
300                     $oSearch->iSearchRank += 2;
301                 }
302
303                 $oSearch->setPoiSearch($iOp, $aSearchTerm['class'], $aSearchTerm['type']);
304                 $aNewWordsetSearches[] = $oSearch;
305             }
306         } elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
307             $iWordID = $aSearchTerm['word_id'];
308             if (sizeof($this->aName)) {
309                 if (($sPhraseType == '' || !$bFirstPhrase)
310                     && $sPhraseType != 'country'))
311                     && !$bHasPartial
312                 ) {
313                     $oSearch = clone $this;
314                     $oSearch->iSearchRank++;
315                     $oSearch->aAddress[$iWordID] = $iWordID;
316                     );
317                     $aNewSearches[] = $oSearch;
318                 }
319                 else {
320                     $this->aFullNameAddress[$iWordID] = $iWordID;
321                 }
322             } else {
323                 $oSearch = clone $this;
324                 $oSearch->iSearchRank++;
325                 $oSearch->aName = array($iWordID => $iWordID);
326                 $aNewSearches[] = $oSearch;
327             }
328         }
329
330         return $aNewSearches;
331     }
332
333     public function extendWithPartialTerm($aSearchTerm, $bStructuredPhrases, $iPhrase, &$aWordFrequencyScores, $aFullTokens)
334     {
335         // Only allow name terms.
336         if (!(isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])) {
337             return array();
338         }
339
340         $aNewSearches = array();
341         $iWordID = $aSearchTerm['word_id'];
342
343         if ((!$bStructuredPhrases || $iPhrase > 0)
344             && sizeof($this->aName)
345             && strpos($aSearchTerm['word_token'], ' ') === false
346         ) {
347             if ($aWordFrequencyScores[$iWordID] < CONST_Max_Word_Frequency) {
348                 $oSearch = clone this;
349                 $oSearch->iSearchRank++;
350                 $oSearch->aAddress[$iWordID] = $iWordID;
351                 $aNewSearches[] = $oSearch;
352             } else {
353                 $oSearch = clone this;
354                 $oSearch->iSearchRank++;
355                 $oSearch->aAddressNonSearch[$iWordID] = $iWordID;
356                 if (preg_match('#^[0-9]+$#', $aSearchTerm['word_token'])) {
357                     $oSearch->iSearchRank += 2;
358                 }
359                 if (sizeof($aFullTokens) {
360                     $oSearch->iSearchRank++;
361                 }
362                 $aNewSearches[] = $oSearch;
363
364                 // revert to the token version?
365                 foreach ($aFullTokens as $aSearchTermToken) {
366                     if (empty($aSearchTermToken['country_code'])
367                         && empty($aSearchTermToken['lat'])
368                         && empty($aSearchTermToken['class'])
369                     ) {
370                         $oSearch = clone $this;
371                         $oSearch->iSearchRank++;
372                         $oSearch->aAddress[$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
373                         $aNewSearches[] = $oSearch;
374                     }
375                 }
376             } 
377         }
378
379         if ((!$this->sPostcode && !$this->aAddress && !$this->aAddressNonSearch)
380             && (!sizeof($this->aName) || $this->iNamePhrase == $iPhrase)
381         ) {
382             $oSearch = clone $this;
383             $oSearch->iSearchRank++;
384             if (!sizeof($this->aName)) {
385                 $aSearch->iSearchRank += 1;
386             }
387             if (preg_match('#^[0-9]+$#', $sSerchTerm['word_token')) {
388                 $oSearch->iSearchRank += 2;
389             }
390             if ($aWordFrequencyScores[$iWordID] < CONST_Max_Word_Frequency) {
391                 $oSearch->aName[$iWordID] = $iWordID;
392             } else {
393                 $aSearch->aNameNonSearch[$iWordID] = $iWordID;
394             }
395             $oSearch->iNamePhrase = $iPhrase;
396             $aNewSearches[] = $aSearch;
397         }
398
399         return $aNewSearches;
400     }
401
402     /////////// Query functions
403
404     public function queryCountry(&$oDB, $sViewboxSQL)
405     {
406         $sSQL = 'SELECT place_id FROM placex ';
407         $sSQL .= "WHERE country_code='".$this->sCountryCode."'";
408         $sSQL .= ' AND rank_search = 4';
409         if ($ViewboxSQL) {
410             $sSQL .= " AND ST_Intersects($sViewboxSQL, geometry)";
411         }
412         $sSQL .= " ORDER BY st_area(geometry) DESC LIMIT 1";
413
414         if (CONST_Debug) var_dump($sSQL);
415
416         return chksql($oDB->getCol($sSQL));
417     }
418
419     public function queryNearbyPoi(&$oDB, $sCountryList, $sViewboxSQL, $sViewboxCentreSQL, $sExcludeSQL, $iLimit)
420     {
421         if (!$this->sClass) {
422             return array();
423         }
424
425         $sPoiTable = $this->poiTable();
426
427         $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = \''.$sPoiTable."'";
428         if (chksql($oDB->getOne($sSQL))) {
429             $sSQL = 'SELECT place_id FROM '.$sPoiTable.' ct';
430             if ($sCountryList) {
431                 $sSQL .= ' JOIN placex USING (place_id)';
432             }
433             if ($this->oNearPoint) {
434                 $sSQL .= ' WHERE '.$this->oNearPoint->withinSQL('ct.centroid');
435             } else {
436                 $sSQL .= " WHERE ST_Contains($sViewboxSQL, ct.centroid)";
437             }
438             if ($sCountryList) {
439                 $sSQL .= " AND country_code in ($sCountryList)";
440             }
441             if ($sExcludeSQL) {
442                 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
443             }
444             if ($sViewboxCentreSQL) {
445                 $sSQL .= " ORDER BY ST_Distance($sViewboxCentreSQL, ct.centroid) ASC";
446             } elseif ($this->oNearPoint) {
447                 $sSQL .= ' ORDER BY '.$this->oNearPoint->distanceSQL('ct.centroid').' ASC';
448             }
449             $sSQL .= " limit $iLimit";
450             if (CONST_Debug) var_dump($sSQL);
451             return chksql($this->oDB->getCol($sSQL));
452         }
453
454         if ($this->oNearPoint) {
455             $sSQL = 'SELECT place_id FROM placex WHERE ';
456             $sSQL .= 'class=\''.$this->sClass."' and type='".$this->sType."'";
457             $sSQL .= ' AND '.$this->oNearPoint->withinSQL('geometry');
458             $sSQL .= ' AND linked_place_id is null';
459             if ($sCountryList) {
460                 $sSQL .= " AND country_code in ($sCountryList)";
461             }
462             $sSQL .= ' ORDER BY '.$this->oNearPoint->distanceSQL('centroid')." ASC";
463             $sSQL .= " LIMIT $iLimit";
464             if (CONST_Debug) var_dump($sSQL);
465             return chksql($this->oDB->getCol($sSQL));
466         }
467
468         return array();
469     }
470
471     public function queryPostcode(&$oDB, $sCountryList, $iLimit)
472     {
473         $sSQL  = 'SELECT p.place_id FROM location_postcode p ';
474
475         if (sizeof($this->aAddress)) {
476             $sSQL .= ', search_name s ';
477             $sSQL .= 'WHERE s.place_id = p.parent_place_id ';
478             $sSQL .= 'AND array_cat(s.nameaddress_vector, s.name_vector)';
479             $sSQL .= '      @> '.getArraySQL($this->aAddress).' AND ';
480         } else {
481             $sSQL .= 'WHERE ';
482         }
483
484         $sSQL .= "p.postcode = '".pg_escape_string(reset($this->$aName))."'";
485         $sCountryTerm = $this->countryCodeSQL('p.country_code', $sCountryList);
486         if ($sCountryTerm) {
487             $sSQL .= ' AND '.$sCountyTerm;
488         }
489         $sSQL .= " LIMIT $iLimit";
490
491         if (CONST_Debug) var_dump($sSQL);
492
493         return chksql($this->oDB->getCol($sSQL));
494     }
495
496     public function queryNamedPlace(&$oDB, $aWordFrequencyScores, $sCountryList, $iMinAddressRank, $iMaxAddressRank, $sExcludeSQL, $sViewboxSmall, $sViewboxLarge, $iLimit)
497     {
498         $aTerms = array();
499         $aOrder = array();
500
501         if ($this->sHouseNumber && sizeof($this->aAddress)) {
502             $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
503             $aOrder[] = ' (';
504             $aOrder[0] .= 'EXISTS(';
505             $aOrder[0] .= '  SELECT place_id';
506             $aOrder[0] .= '  FROM placex';
507             $aOrder[0] .= '  WHERE parent_place_id = search_name.place_id';
508             $aOrder[0] .= "    AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
509             $aOrder[0] .= '  LIMIT 1';
510             $aOrder[0] .= ') ';
511             // also housenumbers from interpolation lines table are needed
512             if (preg_match('/[0-9]+/', $this->sHouseNumber)) {
513                 $iHouseNumber = intval($this->sHouseNumber);
514                 $aOrder[0] .= 'OR EXISTS(';
515                 $aOrder[0] .= '  SELECT place_id ';
516                 $aOrder[0] .= '  FROM location_property_osmline ';
517                 $aOrder[0] .= '  WHERE parent_place_id = search_name.place_id';
518                 $aOrder[0] .= '    AND startnumber is not NULL';
519                 $aOrder[0] .= '    AND '.$iHouseNumber.'>=startnumber ';
520                 $aOrder[0] .= '    AND '.$iHouseNumber.'<=endnumber ';
521                 $aOrder[0] .= '  LIMIT 1';
522                 $aOrder[0] .= ')';
523             }
524             $aOrder[0] .= ') DESC';
525         }
526
527         if (sizeof($this->aName)) {
528             $aTerms[] = 'name_vector @> '.getArraySQL($this->aName);
529         }
530         if (sizeof($this->aAddress)) {
531             // For infrequent name terms disable index usage for address
532             if (CONST_Search_NameOnlySearchFrequencyThreshold
533                 && sizeof($this->aName) == 1
534                 && $aWordFrequencyScores[$this->aName[reset($this->aName)]]
535                      < CONST_Search_NameOnlySearchFrequencyThreshold
536             ) {
537                 $aTerms[] = 'array_cat(nameaddress_vector,ARRAY[]::integer[]) @> '.getArraySQL($this->aAddress);
538             } else {
539                 $aTerms[] = 'nameaddress_vector @> '.getArraySQL($this->aAddress);
540             }
541         }
542
543         $sCountryTerm = $this->countryCodeSQL('p.country_code', $sCountryList);
544         if ($sCountryTerm) {
545             $aTerms[] = $sCountryTerm;
546         }
547
548         if ($this->sHouseNumber) {
549             $aTerms[] = "address_rank between 16 and 27";
550         } elseif (!$this->sClass || $this->iOperator == Operator::NAME) {
551             if ($iMinAddressRank > 0) {
552                 $aTerms[] = "address_rank >= ".$iMinAddressRank;
553             }
554             if ($iMaxAddressRank < 30) {
555                 $aTerms[] = "address_rank <= ".$iMaxAddressRank;
556             }
557         }
558
559         if ($this->oNearPoint) {
560             $aTerms[] = $this->oNearPoint->withinSQL('centroid');
561             $aOrder[] = $this->oNearPoint->distanceSQL('centroid');
562         } elseif ($this->sPostcode) {
563             if (!sizeof($this->aAddress)) {
564                 $aTerms[] = "EXISTS(SELECT place_id FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."' AND ST_DWithin(search_name.centroid, p.geometry, 0.1))";
565             } else {
566                 $aOrder[] = "(SELECT min(ST_Distance(search_name.centroid, p.geometry)) FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."')";
567             }
568         }
569
570         if ($sExcludeSQL) {
571             $aTerms = 'place_id not in ('.$sExcludeSQL.')';
572         }
573
574         if ($sViewboxSmall) {
575            $aTerms[] = 'centroid && '.$sViewboxSmall;
576         }
577
578         if ($this->oNearPoint) {
579             $aOrder[] = $this->oNearPoint->distanceSQL('centroid');
580         }
581
582         if ($this->sHouseNumber) {
583             $sImportanceSQL = '- abs(26 - address_rank) + 3';
584         } else {
585             $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75-(search_rank::float/40) ELSE importance END)';
586         }
587         if ($sViewboxSmall) {
588             $sImportanceSQL .= " * CASE WHEN ST_Contains($sViewboxSmall, centroid) THEN 1 ELSE 0.5 END";
589         }
590         if ($sViewboxLarge) {
591             $sImportanceSQL .= " * CASE WHEN ST_Contains($sViewboxLarge, centroid) THEN 1 ELSE 0.5 END";
592         }
593         $aOrder[] = "$sImportanceSQL DESC";
594
595         if (sizeof($this->aFullNameAddress)) {
596             $sExactMatchSQL = ' ( ';
597             $sExactMatchSQL .= ' SELECT count(*) FROM ( ';
598             $sExactMatchSQL .= '  SELECT unnest('.getArraySQL($this->aFullNameAddress).')';
599             $sExactMatchSQL .= '    INTERSECT ';
600             $sExactMatchSQL .= '  SELECT unnest(nameaddress_vector)';
601             $sExactMatchSQL .= ' ) s';
602             $sExactMatchSQL .= ') as exactmatch';
603             $aOrder[] = 'exactmatch DESC';
604         } else {
605             $sExactMatchSQL = '0::int as exactmatch';
606         }
607
608         if ($this->sHouseNumber || $this->sClass) {
609             $iLimit = 20;
610         }
611
612         if (sizeof($aTerms)) {
613             $sSQL = 'SELECT place_id,'.$sExactMatchSQL;
614             $sSQL .= ' FROM search_name';
615             $sSQL .= ' WHERE '.join(' and ', $aTerms);
616             $sSQL .= ' ORDER BY '.join(', ', $aOrder);
617             $sSQL .= ' LIMIT '.$iLimit;
618
619             if (CONST_Debug) var_dump($sSQL);
620
621             return chksql(
622                 $this->oDB->getAll($sSQL),
623                 "Could not get places for search terms."
624             );
625         }
626
627         return array();
628     }
629
630
631     public function queryHouseNumber(&$oDB, $aRoadPlaceIDs, $sExcludeSQL, $iLimit)
632     {
633         $sPlaceIDs = join(',', $aRoadPlaceIDs);
634
635         $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
636         $sSQL = 'SELECT place_id FROM placex ';
637         $sSQL .= 'WHERE parent_place_id in ('.$sPlaceIDs.')';
638         $sSQL .= "  AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
639         if ($sExcludeSQL) {
640             $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
641         }
642         $sSQL .= " LIMIT $iLimit";
643
644         if (CONST_Debug) var_dump($sSQL);
645
646         $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
647
648         if (sizeof($aPlaceIDs)) {
649             return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
650         }
651
652         $bIsIntHouseNumber= (bool) preg_match('/[0-9]+/', $this->sHouseNumber);
653         $iHousenumber = intval($this->sHouseNumber);
654         if ($bIsIntHouseNumber) {
655             // if nothing found, search in the interpolation line table
656             $sSQL = 'SELECT distinct place_id FROM location_property_osmline';
657             $sSQL .= ' WHERE startnumber is not NULL';
658             $sSQL .= '  AND parent_place_id in ('.$sPlaceIDs.') AND (';
659             if ($iHousenumber % 2 == 0) {
660                 // If housenumber is even, look for housenumber in streets
661                 // with interpolationtype even or all.
662                 $sSQL .= "interpolationtype='even'";
663             } else {
664                 // Else look for housenumber with interpolationtype odd or all.
665                 $sSQL .= "interpolationtype='odd'";
666             }
667             $sSQL .= " or interpolationtype='all') and ";
668             $sSQL .= $iHousenumber.">=startnumber and ";
669             $sSQL .= $iHousenumber."<=endnumber";
670
671             if ($sExcludeSQL)) {
672                 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
673             }
674             $sSQL .= " limit $iLimit";
675
676             if (CONST_Debug) var_dump($sSQL);
677
678             $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0));
679
680             if (sizeof($aPlaceIDs)) {
681                 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
682             }
683         }
684
685         // If nothing found try the aux fallback table
686         if (CONST_Use_Aux_Location_data) {
687             $sSQL = 'SELECT place_id FROM location_property_aux';
688             $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.')';
689             $sSQL .= " AND housenumber = '".$this->sHouseNumber."'";
690             if ($sExcludeSQL) {
691                 $sSQL .= " AND place_id not in ($sExcludeSQL)";
692             }
693             $sSQL .= " limit $iLimit";
694
695             if (CONST_Debug) var_dump($sSQL);
696
697             $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
698
699             if (sizeof($aPlaceIDs)) {
700                 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
701             }
702         }
703
704         // If nothing found then search in Tiger data (location_property_tiger)
705         if (CONST_Use_US_Tiger_Data && $bIsIntHouseNumber) {
706             $sSQL = 'SELECT distinct place_id FROM location_property_tiger';
707             $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.') and (';
708             if ($iHousenumber % 2 == 0) {
709                 $sSQL .= "interpolationtype='even'";
710             } else {
711                 $sSQL .= "interpolationtype='odd'";
712             }
713             $sSQL .= " or interpolationtype='all') and ";
714             $sSQL .= $iHousenumber.">=startnumber and ";
715             $sSQL .= $iHousenumber."<=endnumber";
716
717             if ($sExcludeSQL) {
718                 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
719             }
720             $sSQL .= " limit $iLimit";
721
722             if (CONST_Debug) var_dump($sSQL);
723
724             $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0));
725
726             if (sizeof($aPlaceIDs)) {
727                 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
728             }
729         }
730
731         return array();
732     }
733
734
735     public function queryPoiByOperator(&$oDB, $aParentIDs, $sExcludeSQL, $iLimit)
736     {
737         $sPlaceIDs = join(',', $aParentIDs);
738         $aClassPlaceIDs = array();
739
740         if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NAME) {
741             // If they were searching for a named class (i.e. 'Kings Head pub')
742             // then we might have an extra match
743             $sSQL = 'SELECT place_id FROM placex ';
744             $sSQL .= " WHERE place_id in ($sPlaceIDs)";
745             $sSQL .= "   AND class='".$this->sClass."' ";
746             $sSQL .= "   AND type='".$this->sType."'";
747             $sSQL .= "   AND linked_place_id is null";
748             $sSQL .= " ORDER BY rank_search ASC ";
749             $sSQL .= " LIMIT $iLimit";
750
751             if (CONST_Debug) var_dump($sSQL);
752
753             $aClassPlaceIDs = chksql($this->oDB->getCol($sSQL));
754         }
755
756         // NEAR and IN are handled the same
757         if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NEAR) {
758             $sClassTable = $this->poiTable();
759             $sSQL = "SELECT count(*) FROM pg_tables WHERE tablename = '$sClassTable'";
760             $bCacheTable = (bool) chksql($this->oDB->getOne($sSQL));
761
762             $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)";
763             if (CONST_Debug) var_dump($sSQL);
764             $iMaxRank = (int)chksql($this->oDB->getOne($sSQL));
765
766             // For state / country level searches the normal radius search doesn't work very well
767             $sPlaceGeom = false;
768             if ($iMaxRank < 9 && $bCacheTable) {
769                 // Try and get a polygon to search in instead
770                 $sSQL = 'SELECT geometry FROM placex';
771                 $sSQL .= " WHERE place_id in ($sPlaceIDs)";
772                 $sSQL .= "   AND rank_search < $iMaxRank + 5";
773                 $sSQL .= "   AND ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon')";
774                 $sSQL .= " ORDER BY rank_search ASC ";
775                 $sSQL .= " LIMIT 1";
776                 if (CONST_Debug) var_dump($sSQL);
777                 $sPlaceGeom = chksql($this->oDB->getOne($sSQL));
778             }
779
780             if ($sPlaceGeom) {
781                 $sPlaceIDs = false;
782             } else {
783                 $iMaxRank += 5;
784                 $sSQL = 'SELECT place_id FROM placex';
785                 $sSQL .= " WHERE place_id in ($sPlaceIDs) and rank_search < $iMaxRank";
786                 if (CONST_Debug) var_dump($sSQL);
787                 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
788                 $sPlaceIDs = join(',', $aPlaceIDs);
789             }
790
791             if ($sPlaceIDs || $sPlaceGeom) {
792                 $fRange = 0.01;
793                 if ($bCacheTable) {
794                     // More efficient - can make the range bigger
795                     $fRange = 0.05;
796
797                     $sOrderBySQL = '';
798                     if ($this->oNearPoint) {
799                         $sOrderBySQL = $this->oNearPoint->distanceSQL('l.centroid');
800                     } elseif ($sPlaceIDs) {
801                         $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
802                     } elseif ($sPlaceGeom) {
803                         $sOrderBySQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
804                     }
805
806                     $sSQL = 'SELECT distinct i.place_id';
807                     if ($sOrderBySQL) {
808                         $sSQL .= ', i.order_term';
809                     }
810                     $sSQL .= ' from (SELECT l.place_id';
811                     if ($sOrderBySQL) {
812                         $sSQL .= ','.$sOrderBySQL.' as order_term';
813                     }
814                     $sSQL .= ' from '.$sClassTable.' as l';
815
816                     if ($sPlaceIDs) {
817                         $sSQL .= ",placex as f WHERE ";
818                         $sSQL .= "f.place_id in ($sPlaceIDs) ";
819                         $sSQL .= " AND ST_DWithin(l.centroid, f.centroid, $fRange)";
820                     } elseif ($sPlaceGeom) {
821                         $sSQL .= " WHERE ST_Contains('$sPlaceGeom', l.centroid)";
822                     }
823
824                     if ($sExcludeSQL) {
825                         $sSQL .= ' AND l.place_id not in ('.$sExcludeSQL.')';
826                     }
827                     $sSQL .= 'limit 300) i ';
828                     if ($sOrderBySQL) {
829                         $sSQL .= 'order by order_term asc';
830                     }
831                     $sSQL .= " limit $iLimit";
832
833                     if (CONST_Debug) var_dump($sSQL);
834
835                     $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL)));
836                 } else {
837                     if ($this->oNearPoint) {
838                         $fRange = $this->oNearPoint->radius();
839                     }
840
841                     $sOrderBySQL = '';
842                     if ($this->oNearPoint) {
843                         $sOrderBySQL = $this->oNearPoint->distanceSQL('l.geometry');
844                     } else {
845                         $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
846                     }
847
848                     $sSQL = 'SELECT distinct l.place_id';
849                     if ($sOrderBySQL) {
850                         $sSQL .= ','.$sOrderBySQL.' as orderterm';
851                     }
852                     $sSQL .= ' FROM placex as l, placex as f';
853                     $sSQL .= " WHERE f.place_id in ($sPlaceIDs)";
854                     $sSQL .= "  AND ST_DWithin(l.geometry, f.centroid, $fRange)";
855                     $sSQL .= "  AND l.class='".$this->sClass."'";
856                     $sSQL .= "  AND l.type='".$this->sType."'";
857                     if ($sExcludeSQL) {
858                         $sSQL .= " AND l.place_id not in (".$sExcludeSQL.")";
859                     }
860                     if ($sOrderBySQL) {
861                         $sSQL .= "ORDER BY orderterm ASC";
862                     }
863                     $sSQL .= " limit $iLimit";
864
865                     if (CONST_Debug) var_dump($sSQL);
866
867                     $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL)));
868                 }
869             }
870         }
871
872         return $aClassPlaceIDs;
873     }
874 };