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