5 require_once(CONST_BasePath.'/lib/SpecialSearchOperator.php');
8 * Description of a single interpretation of a search query.
10 class SearchDescription
12 /// Ranking how well the description fits the query.
13 private $iSearchRank = 0;
14 /// Country code of country the result must belong to.
15 private $sCountryCode = '';
16 /// List of word ids making up the name of the object.
17 private $aName = array();
18 /// List of word ids making up the address of the object.
19 private $aAddress = array();
20 /// Subset of word ids of full words making up the address.
21 private $aFullNameAddress = array();
22 /// List of word ids that appear in the name but should be ignored.
23 private $aNameNonSearch = array();
24 /// List of word ids that appear in the address but should be ignored.
25 private $aAddressNonSearch = array();
26 /// Kind of search for special searches, see Nominatim::Operator.
27 private $iOperator = Operator::NONE;
28 /// Class of special feature to search for.
30 /// Type of special feature to search for.
32 /// Housenumber of the object.
33 private $sHouseNumber = '';
34 /// Postcode for the object.
35 private $sPostcode = '';
36 /// Geographic search area.
37 private $oNearPoint = false;
39 // Temporary values used while creating the search description.
41 /// Index of phrase currently processed
42 private $iNamePhrase = -1;
45 public function getRank()
47 return $this->iSearchRank;
50 public function addToRank($iAddRank)
52 $this->iSearchRank += $iAddRank;
53 return $this->iSearchRank;
56 public function getPostCode()
58 return $this->sPostcode;
61 public function setNear(&$oNearPoint)
63 $this->oNearPoint = $oNearPoint;
66 public function setPoiSearch($iOperator, $sClass, $sType)
68 $this->iOperator = $iOperator;
69 $this->sClass = $sClass;
70 $this->sType = $sType;
73 public function isNamedSearch()
75 return sizeof($this->aName) > 0 || sizeof($this->aAddress) > 0;
78 public function isCountrySearch()
80 return $this->sCountryCode && sizeof($this->aName) == 0
81 && !$this->iOperator && !$this->oNearPoint;
84 public function isNearSearch()
86 return (bool) $this->oNearPoint;
89 public function isPoiSearch()
91 return (bool) $this->sClass;
94 public function looksLikeFullAddress()
96 return sizeof($this->aName)
97 && (sizeof($this->aAddress || $this->sCountryCode))
98 && preg_match('/[0-9]+/', $this->sHouseNumber);
101 public function isOperator($iType)
103 return $this->iOperator == $iType;
106 public function hasHouseNumber()
108 return (bool) $this->sHouseNumber;
111 private function poiTable()
113 return 'place_classtype_'.$this->sClass.'_'.$this->sType;
116 public function countryCodeSQL($sVar, $sCountryList)
118 if ($this->sCountryCode) {
119 return $sVar.' = \''.$this->sCountryCode."'";
122 return $sVar.' in ('.$sCountryList.')';
128 public function hasOperator()
130 return $this->iOperator != Operator::NONE;
133 public function extractKeyValuePairs($sQuery)
135 // Search for terms of kind [<key>=<value>].
137 '/\\[([\\w_]*)=([\\w_]*)\\]/',
143 foreach ($aSpecialTermsRaw as $aTerm) {
144 $sQuery = str_replace($aTerm[0], ' ', $sQuery);
145 if (!$this->hasOperator()) {
146 $this->setPoiSearch(Operator::TYPE, $aTerm[1], $aTerm[2]);
153 public function isValidSearch(&$aCountryCodes)
155 if (!sizeof($this->aName)) {
156 if ($this->sHouseNumber) {
161 && $this->sCountryCode
162 && !in_array($this->sCountryCode, $aCountryCodes)
170 /////////// Search building functions
173 public function extendWithFullTerm($aSearchTerm, $bWordInQuery, $bHasPartial, $sPhraseType, $bFirstToken, $bFirstPhrase, $bLastToken, &$iGlobalRank)
175 $aNewSearches = array();
177 if (($sPhraseType == '' || $sPhraseType == 'country')
178 && !empty($aSearchTerm['country_code'])
179 && $aSearchTerm['country_code'] != '0'
181 if (!$this->sCountryCode) {
182 $oSearch = clone $this;
183 $oSearch->iSearchRank++;
184 $oSearch->sCountryCode = $aSearchTerm['country_code'];
185 // Country is almost always at the end of the string
186 // - increase score for finding it anywhere else (optimisation)
188 $oSearch->iSearchRank += 5;
190 $aNewSearches[] = $oSearch;
192 // If it is at the beginning, we can be almost sure that
193 // the terms are in the wrong order. Increase score for all searches.
198 } elseif (($sPhraseType == '' || $sPhraseType == 'postalcode')
199 && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'postcode'
201 // We need to try the case where the postal code is the primary element
202 // (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode)
204 if (!$this->sPostcode && $bWordInQuery
205 && pg_escape_string($aSearchTerm['word']) == $aSearchTerm['word']
207 // If we have structured search or this is the first term,
208 // make the postcode the primary search element.
209 if ($this->iOperator == Operator::NONE
210 && ($sPhraseType == 'postalcode' || $bFirstToken)
212 $oSearch = clone $this;
213 $oSearch->iSearchRank++;
214 $oSearch->iOperator = Operator::POSTCODE;
215 $oSearch->aAddress = array_merge($this->aAddress, $this->aName);
217 array($aSearchTerm['word_id'] => $aSearchTerm['word']);
218 $aNewSearches[] = $oSearch;
221 // If we have a structured search or this is not the first term,
222 // add the postcode as an addendum.
223 if ($this->iOperator != Operator::POSTCODE
224 && ($sPhraseType == 'postalcode' || sizeof($this->aName))
226 $oSearch = clone $this;
227 $oSearch->iSearchRank++;
228 $oSearch->sPostcode = $aSearchTerm['word'];
229 $aNewSearches[] = $oSearch;
232 } elseif (($sPhraseType == '' || $sPhraseType == 'street')
233 && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house'
235 if (!$this->sHouseNumber && $this->iOperator != Operator::POSTCODE) {
236 $oSearch = clone $this;
237 $oSearch->iSearchRank++;
238 $oSearch->sHouseNumber = trim($aSearchTerm['word_token']);
239 // sanity check: if the housenumber is not mainly made
240 // up of numbers, add a penalty
241 if (preg_match_all("/[^0-9]/", $oSearch->sHouseNumber, $aMatches) > 2) {
242 $oSearch->iSearchRank++;
244 if (!isset($aSearchTerm['word_id'])) {
245 $oSearch->iSearchRank++;
247 // also must not appear in the middle of the address
248 if (sizeof($this->aAddress) || sizeof($this->aAddressNonSearch)) {
249 $oSearch->iSearchRank++;
251 $aNewSearches[] = $oSearch;
253 } elseif ($sPhraseType == ''
254 && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null
256 // require a normalized exact match of the term
257 // if we have the normalizer version of the query
259 if ($this->iOperator == Operator::NONE
260 && (isset($aSearchTerm['word']) && $aSearchTerm['word'])
263 $oSearch = clone $this;
264 $oSearch->iSearchRank++;
266 $iOp = Operator::NEAR; // near == in for the moment
267 if ($aSearchTerm['operator'] == '') {
268 if (sizeof($this->aName)) {
269 $iOp = Operator::NAME;
271 $oSearch->iSearchRank += 2;
274 $oSearch->setPoiSearch($iOp, $aSearchTerm['class'], $aSearchTerm['type']);
275 $aNewSearches[] = $oSearch;
277 } elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
278 $iWordID = $aSearchTerm['word_id'];
279 if (sizeof($this->aName)) {
280 if (($sPhraseType == '' || !$bFirstPhrase)
281 && $sPhraseType != 'country'
284 $oSearch = clone $this;
285 $oSearch->iSearchRank++;
286 $oSearch->aAddress[$iWordID] = $iWordID;
287 $aNewSearches[] = $oSearch;
289 $this->aFullNameAddress[$iWordID] = $iWordID;
292 $oSearch = clone $this;
293 $oSearch->iSearchRank++;
294 $oSearch->aName = array($iWordID => $iWordID);
295 $aNewSearches[] = $oSearch;
299 return $aNewSearches;
302 public function extendWithPartialTerm($aSearchTerm, $bStructuredPhrases, $iPhrase, &$aWordFrequencyScores, $aFullTokens)
304 // Only allow name terms.
305 if (!(isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])) {
309 $aNewSearches = array();
310 $iWordID = $aSearchTerm['word_id'];
312 if ((!$bStructuredPhrases || $iPhrase > 0)
313 && sizeof($this->aName)
314 && strpos($aSearchTerm['word_token'], ' ') === false
316 if ($aWordFrequencyScores[$iWordID] < CONST_Max_Word_Frequency) {
317 $oSearch = clone $this;
318 $oSearch->iSearchRank++;
319 $oSearch->aAddress[$iWordID] = $iWordID;
320 $aNewSearches[] = $oSearch;
322 $oSearch = clone $this;
323 $oSearch->iSearchRank++;
324 $oSearch->aAddressNonSearch[$iWordID] = $iWordID;
325 if (preg_match('#^[0-9]+$#', $aSearchTerm['word_token'])) {
326 $oSearch->iSearchRank += 2;
328 if (sizeof($aFullTokens)) {
329 $oSearch->iSearchRank++;
331 $aNewSearches[] = $oSearch;
333 // revert to the token version?
334 foreach ($aFullTokens as $aSearchTermToken) {
335 if (empty($aSearchTermToken['country_code'])
336 && empty($aSearchTermToken['lat'])
337 && empty($aSearchTermToken['class'])
339 $oSearch = clone $this;
340 $oSearch->iSearchRank++;
341 $oSearch->aAddress[$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
342 $aNewSearches[] = $oSearch;
348 if ((!$this->sPostcode && !$this->aAddress && !$this->aAddressNonSearch)
349 && (!sizeof($this->aName) || $this->iNamePhrase == $iPhrase)
351 $oSearch = clone $this;
352 $oSearch->iSearchRank++;
353 if (!sizeof($this->aName)) {
354 $oSearch->iSearchRank += 1;
356 if (preg_match('#^[0-9]+$#', $aSearchTerm['word_token'])) {
357 $oSearch->iSearchRank += 2;
359 if ($aWordFrequencyScores[$iWordID] < CONST_Max_Word_Frequency) {
360 $oSearch->aName[$iWordID] = $iWordID;
362 $oSearch->aNameNonSearch[$iWordID] = $iWordID;
364 $oSearch->iNamePhrase = $iPhrase;
365 $aNewSearches[] = $oSearch;
368 return $aNewSearches;
371 /////////// Query functions
374 public function queryCountry(&$oDB, $sViewboxSQL)
376 $sSQL = 'SELECT place_id FROM placex ';
377 $sSQL .= "WHERE country_code='".$this->sCountryCode."'";
378 $sSQL .= ' AND rank_search = 4';
380 $sSQL .= " AND ST_Intersects($sViewboxSQL, geometry)";
382 $sSQL .= " ORDER BY st_area(geometry) DESC LIMIT 1";
384 if (CONST_Debug) var_dump($sSQL);
386 return chksql($oDB->getCol($sSQL));
389 public function queryNearbyPoi(&$oDB, $sCountryList, $sViewboxSQL, $sViewboxCentreSQL, $sExcludeSQL, $iLimit)
391 if (!$this->sClass) {
395 $sPoiTable = $this->poiTable();
397 $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = \''.$sPoiTable."'";
398 if (chksql($oDB->getOne($sSQL))) {
399 $sSQL = 'SELECT place_id FROM '.$sPoiTable.' ct';
401 $sSQL .= ' JOIN placex USING (place_id)';
403 if ($this->oNearPoint) {
404 $sSQL .= ' WHERE '.$this->oNearPoint->withinSQL('ct.centroid');
406 $sSQL .= " WHERE ST_Contains($sViewboxSQL, ct.centroid)";
409 $sSQL .= " AND country_code in ($sCountryList)";
412 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
414 if ($sViewboxCentreSQL) {
415 $sSQL .= " ORDER BY ST_Distance($sViewboxCentreSQL, ct.centroid) ASC";
416 } elseif ($this->oNearPoint) {
417 $sSQL .= ' ORDER BY '.$this->oNearPoint->distanceSQL('ct.centroid').' ASC';
419 $sSQL .= " limit $iLimit";
420 if (CONST_Debug) var_dump($sSQL);
421 return chksql($oDB->getCol($sSQL));
424 if ($this->oNearPoint) {
425 $sSQL = 'SELECT place_id FROM placex WHERE ';
426 $sSQL .= 'class=\''.$this->sClass."' and type='".$this->sType."'";
427 $sSQL .= ' AND '.$this->oNearPoint->withinSQL('geometry');
428 $sSQL .= ' AND linked_place_id is null';
430 $sSQL .= " AND country_code in ($sCountryList)";
432 $sSQL .= ' ORDER BY '.$this->oNearPoint->distanceSQL('centroid')." ASC";
433 $sSQL .= " LIMIT $iLimit";
434 if (CONST_Debug) var_dump($sSQL);
435 return chksql($oDB->getCol($sSQL));
441 public function queryPostcode(&$oDB, $sCountryList, $iLimit)
443 $sSQL = 'SELECT p.place_id FROM location_postcode p ';
445 if (sizeof($this->aAddress)) {
446 $sSQL .= ', search_name s ';
447 $sSQL .= 'WHERE s.place_id = p.parent_place_id ';
448 $sSQL .= 'AND array_cat(s.nameaddress_vector, s.name_vector)';
449 $sSQL .= ' @> '.getArraySQL($this->aAddress).' AND ';
454 $sSQL .= "p.postcode = '".reset($this->aName)."'";
455 $sCountryTerm = $this->countryCodeSQL('p.country_code', $sCountryList);
457 $sSQL .= ' AND '.$sCountryTerm;
459 $sSQL .= " LIMIT $iLimit";
461 if (CONST_Debug) var_dump($sSQL);
463 return chksql($oDB->getCol($sSQL));
466 public function queryNamedPlace(&$oDB, $aWordFrequencyScores, $sCountryList, $iMinAddressRank, $iMaxAddressRank, $sExcludeSQL, $sViewboxSmall, $sViewboxLarge, $iLimit)
471 if ($this->sHouseNumber && sizeof($this->aAddress)) {
472 $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
474 $aOrder[0] .= 'EXISTS(';
475 $aOrder[0] .= ' SELECT place_id';
476 $aOrder[0] .= ' FROM placex';
477 $aOrder[0] .= ' WHERE parent_place_id = search_name.place_id';
478 $aOrder[0] .= " AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
479 $aOrder[0] .= ' LIMIT 1';
481 // also housenumbers from interpolation lines table are needed
482 if (preg_match('/[0-9]+/', $this->sHouseNumber)) {
483 $iHouseNumber = intval($this->sHouseNumber);
484 $aOrder[0] .= 'OR EXISTS(';
485 $aOrder[0] .= ' SELECT place_id ';
486 $aOrder[0] .= ' FROM location_property_osmline ';
487 $aOrder[0] .= ' WHERE parent_place_id = search_name.place_id';
488 $aOrder[0] .= ' AND startnumber is not NULL';
489 $aOrder[0] .= ' AND '.$iHouseNumber.'>=startnumber ';
490 $aOrder[0] .= ' AND '.$iHouseNumber.'<=endnumber ';
491 $aOrder[0] .= ' LIMIT 1';
494 $aOrder[0] .= ') DESC';
497 if (sizeof($this->aName)) {
498 $aTerms[] = 'name_vector @> '.getArraySQL($this->aName);
500 if (sizeof($this->aAddress)) {
501 // For infrequent name terms disable index usage for address
502 if (CONST_Search_NameOnlySearchFrequencyThreshold
503 && sizeof($this->aName) == 1
504 && $aWordFrequencyScores[$this->aName[reset($this->aName)]]
505 < CONST_Search_NameOnlySearchFrequencyThreshold
507 $aTerms[] = 'array_cat(nameaddress_vector,ARRAY[]::integer[]) @> '.getArraySQL($this->aAddress);
509 $aTerms[] = 'nameaddress_vector @> '.getArraySQL($this->aAddress);
513 $sCountryTerm = $this->countryCodeSQL('country_code', $sCountryList);
515 $aTerms[] = $sCountryTerm;
518 if ($this->sHouseNumber) {
519 $aTerms[] = "address_rank between 16 and 27";
520 } elseif (!$this->sClass || $this->iOperator == Operator::NAME) {
521 if ($iMinAddressRank > 0) {
522 $aTerms[] = "address_rank >= ".$iMinAddressRank;
524 if ($iMaxAddressRank < 30) {
525 $aTerms[] = "address_rank <= ".$iMaxAddressRank;
529 if ($this->oNearPoint) {
530 $aTerms[] = $this->oNearPoint->withinSQL('centroid');
531 $aOrder[] = $this->oNearPoint->distanceSQL('centroid');
532 } elseif ($this->sPostcode) {
533 if (!sizeof($this->aAddress)) {
534 $aTerms[] = "EXISTS(SELECT place_id FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."' AND ST_DWithin(search_name.centroid, p.geometry, 0.1))";
536 $aOrder[] = "(SELECT min(ST_Distance(search_name.centroid, p.geometry)) FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."')";
541 $aTerms[] = 'place_id not in ('.$sExcludeSQL.')';
544 if ($sViewboxSmall) {
545 $aTerms[] = 'centroid && '.$sViewboxSmall;
548 if ($this->oNearPoint) {
549 $aOrder[] = $this->oNearPoint->distanceSQL('centroid');
552 if ($this->sHouseNumber) {
553 $sImportanceSQL = '- abs(26 - address_rank) + 3';
555 $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75-(search_rank::float/40) ELSE importance END)';
557 if ($sViewboxSmall) {
558 $sImportanceSQL .= " * CASE WHEN ST_Contains($sViewboxSmall, centroid) THEN 1 ELSE 0.5 END";
560 if ($sViewboxLarge) {
561 $sImportanceSQL .= " * CASE WHEN ST_Contains($sViewboxLarge, centroid) THEN 1 ELSE 0.5 END";
563 $aOrder[] = "$sImportanceSQL DESC";
565 if (sizeof($this->aFullNameAddress)) {
566 $sExactMatchSQL = ' ( ';
567 $sExactMatchSQL .= ' SELECT count(*) FROM ( ';
568 $sExactMatchSQL .= ' SELECT unnest('.getArraySQL($this->aFullNameAddress).')';
569 $sExactMatchSQL .= ' INTERSECT ';
570 $sExactMatchSQL .= ' SELECT unnest(nameaddress_vector)';
571 $sExactMatchSQL .= ' ) s';
572 $sExactMatchSQL .= ') as exactmatch';
573 $aOrder[] = 'exactmatch DESC';
575 $sExactMatchSQL = '0::int as exactmatch';
578 if ($this->sHouseNumber || $this->sClass) {
582 if (sizeof($aTerms)) {
583 $sSQL = 'SELECT place_id,'.$sExactMatchSQL;
584 $sSQL .= ' FROM search_name';
585 $sSQL .= ' WHERE '.join(' and ', $aTerms);
586 $sSQL .= ' ORDER BY '.join(', ', $aOrder);
587 $sSQL .= ' LIMIT '.$iLimit;
589 if (CONST_Debug) var_dump($sSQL);
593 "Could not get places for search terms."
601 public function queryHouseNumber(&$oDB, $aRoadPlaceIDs, $sExcludeSQL, $iLimit)
603 $sPlaceIDs = join(',', $aRoadPlaceIDs);
605 $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
606 $sSQL = 'SELECT place_id FROM placex ';
607 $sSQL .= 'WHERE parent_place_id in ('.$sPlaceIDs.')';
608 $sSQL .= " AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
610 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
612 $sSQL .= " LIMIT $iLimit";
614 if (CONST_Debug) var_dump($sSQL);
616 $aPlaceIDs = chksql($oDB->getCol($sSQL));
618 if (sizeof($aPlaceIDs)) {
619 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
622 $bIsIntHouseNumber= (bool) preg_match('/[0-9]+/', $this->sHouseNumber);
623 $iHousenumber = intval($this->sHouseNumber);
624 if ($bIsIntHouseNumber) {
625 // if nothing found, search in the interpolation line table
626 $sSQL = 'SELECT distinct place_id FROM location_property_osmline';
627 $sSQL .= ' WHERE startnumber is not NULL';
628 $sSQL .= ' AND parent_place_id in ('.$sPlaceIDs.') AND (';
629 if ($iHousenumber % 2 == 0) {
630 // If housenumber is even, look for housenumber in streets
631 // with interpolationtype even or all.
632 $sSQL .= "interpolationtype='even'";
634 // Else look for housenumber with interpolationtype odd or all.
635 $sSQL .= "interpolationtype='odd'";
637 $sSQL .= " or interpolationtype='all') and ";
638 $sSQL .= $iHousenumber.">=startnumber and ";
639 $sSQL .= $iHousenumber."<=endnumber";
642 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
644 $sSQL .= " limit $iLimit";
646 if (CONST_Debug) var_dump($sSQL);
648 $aPlaceIDs = chksql($oDB->getCol($sSQL, 0));
650 if (sizeof($aPlaceIDs)) {
651 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
655 // If nothing found try the aux fallback table
656 if (CONST_Use_Aux_Location_data) {
657 $sSQL = 'SELECT place_id FROM location_property_aux';
658 $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.')';
659 $sSQL .= " AND housenumber = '".$this->sHouseNumber."'";
661 $sSQL .= " AND place_id not in ($sExcludeSQL)";
663 $sSQL .= " limit $iLimit";
665 if (CONST_Debug) var_dump($sSQL);
667 $aPlaceIDs = chksql($oDB->getCol($sSQL));
669 if (sizeof($aPlaceIDs)) {
670 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
674 // If nothing found then search in Tiger data (location_property_tiger)
675 if (CONST_Use_US_Tiger_Data && $bIsIntHouseNumber) {
676 $sSQL = 'SELECT distinct place_id FROM location_property_tiger';
677 $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.') and (';
678 if ($iHousenumber % 2 == 0) {
679 $sSQL .= "interpolationtype='even'";
681 $sSQL .= "interpolationtype='odd'";
683 $sSQL .= " or interpolationtype='all') and ";
684 $sSQL .= $iHousenumber.">=startnumber and ";
685 $sSQL .= $iHousenumber."<=endnumber";
688 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
690 $sSQL .= " limit $iLimit";
692 if (CONST_Debug) var_dump($sSQL);
694 $aPlaceIDs = chksql($oDB->getCol($sSQL, 0));
696 if (sizeof($aPlaceIDs)) {
697 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
705 public function queryPoiByOperator(&$oDB, $aParentIDs, $sExcludeSQL, $iLimit)
707 $sPlaceIDs = join(',', $aParentIDs);
708 $aClassPlaceIDs = array();
710 if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NAME) {
711 // If they were searching for a named class (i.e. 'Kings Head pub')
712 // then we might have an extra match
713 $sSQL = 'SELECT place_id FROM placex ';
714 $sSQL .= " WHERE place_id in ($sPlaceIDs)";
715 $sSQL .= " AND class='".$this->sClass."' ";
716 $sSQL .= " AND type='".$this->sType."'";
717 $sSQL .= " AND linked_place_id is null";
718 $sSQL .= " ORDER BY rank_search ASC ";
719 $sSQL .= " LIMIT $iLimit";
721 if (CONST_Debug) var_dump($sSQL);
723 $aClassPlaceIDs = chksql($oDB->getCol($sSQL));
726 // NEAR and IN are handled the same
727 if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NEAR) {
728 $sClassTable = $this->poiTable();
729 $sSQL = "SELECT count(*) FROM pg_tables WHERE tablename = '$sClassTable'";
730 $bCacheTable = (bool) chksql($oDB->getOne($sSQL));
732 $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)";
733 if (CONST_Debug) var_dump($sSQL);
734 $iMaxRank = (int)chksql($oDB->getOne($sSQL));
736 // For state / country level searches the normal radius search doesn't work very well
738 if ($iMaxRank < 9 && $bCacheTable) {
739 // Try and get a polygon to search in instead
740 $sSQL = 'SELECT geometry FROM placex';
741 $sSQL .= " WHERE place_id in ($sPlaceIDs)";
742 $sSQL .= " AND rank_search < $iMaxRank + 5";
743 $sSQL .= " AND ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon')";
744 $sSQL .= " ORDER BY rank_search ASC ";
746 if (CONST_Debug) var_dump($sSQL);
747 $sPlaceGeom = chksql($oDB->getOne($sSQL));
754 $sSQL = 'SELECT place_id FROM placex';
755 $sSQL .= " WHERE place_id in ($sPlaceIDs) and rank_search < $iMaxRank";
756 if (CONST_Debug) var_dump($sSQL);
757 $aPlaceIDs = chksql($oDB->getCol($sSQL));
758 $sPlaceIDs = join(',', $aPlaceIDs);
761 if ($sPlaceIDs || $sPlaceGeom) {
764 // More efficient - can make the range bigger
768 if ($this->oNearPoint) {
769 $sOrderBySQL = $this->oNearPoint->distanceSQL('l.centroid');
770 } elseif ($sPlaceIDs) {
771 $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
772 } elseif ($sPlaceGeom) {
773 $sOrderBySQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
776 $sSQL = 'SELECT distinct i.place_id';
778 $sSQL .= ', i.order_term';
780 $sSQL .= ' from (SELECT l.place_id';
782 $sSQL .= ','.$sOrderBySQL.' as order_term';
784 $sSQL .= ' from '.$sClassTable.' as l';
787 $sSQL .= ",placex as f WHERE ";
788 $sSQL .= "f.place_id in ($sPlaceIDs) ";
789 $sSQL .= " AND ST_DWithin(l.centroid, f.centroid, $fRange)";
790 } elseif ($sPlaceGeom) {
791 $sSQL .= " WHERE ST_Contains('$sPlaceGeom', l.centroid)";
795 $sSQL .= ' AND l.place_id not in ('.$sExcludeSQL.')';
797 $sSQL .= 'limit 300) i ';
799 $sSQL .= 'order by order_term asc';
801 $sSQL .= " limit $iLimit";
803 if (CONST_Debug) var_dump($sSQL);
805 $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($oDB->getCol($sSQL)));
807 if ($this->oNearPoint) {
808 $fRange = $this->oNearPoint->radius();
812 if ($this->oNearPoint) {
813 $sOrderBySQL = $this->oNearPoint->distanceSQL('l.geometry');
815 $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
818 $sSQL = 'SELECT distinct l.place_id';
820 $sSQL .= ','.$sOrderBySQL.' as orderterm';
822 $sSQL .= ' FROM placex as l, placex as f';
823 $sSQL .= " WHERE f.place_id in ($sPlaceIDs)";
824 $sSQL .= " AND ST_DWithin(l.geometry, f.centroid, $fRange)";
825 $sSQL .= " AND l.class='".$this->sClass."'";
826 $sSQL .= " AND l.type='".$this->sType."'";
828 $sSQL .= " AND l.place_id not in (".$sExcludeSQL.")";
831 $sSQL .= "ORDER BY orderterm ASC";
833 $sSQL .= " limit $iLimit";
835 if (CONST_Debug) var_dump($sSQL);
837 $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($oDB->getCol($sSQL)));
842 return $aClassPlaceIDs;
846 /////////// Sort functions
849 public static function bySearchRank($a, $b)
851 if ($a->iSearchRank == $b->iSearchRank) {
852 return $a->iOperator + strlen($a->sHouseNumber)
853 - $b->iOperator - strlen($b->sHouseNumber);
856 return $a->iSearchRank < $b->iSearchRank ? -1 : 1;
859 //////////// Debugging functions
862 public function dumpAsHtmlTableRow(&$aWordIDs)
864 $kf = function ($k) use (&$aWordIDs) {
865 return $aWordIDs[$k];
869 echo "<td>$this->iSearchRank</td>";
870 echo "<td>".join(', ', array_map($kf, $this->aName))."</td>";
871 echo "<td>".join(', ', array_map($kf, $this->aNameNonSearch))."</td>";
872 echo "<td>".join(', ', array_map($kf, $this->aAddress))."</td>";
873 echo "<td>".join(', ', array_map($kf, $this->aAddressNonSearch))."</td>";
874 echo "<td>".$this->sCountryCode."</td>";
875 echo "<td>".Operator::toString($this->iOperator)."</td>";
876 echo "<td>".$this->sClass."</td>";
877 echo "<td>".$this->sType."</td>";
878 echo "<td>".$this->sPostcode."</td>";
879 echo "<td>".$this->sHouseNumber."</td>";
881 if ($this->oNearPoint) {
882 echo "<td>".$this->oNearPoint->lat()."</td>";
883 echo "<td>".$this->oNearPoint->lon()."</td>";
884 echo "<td>".$this->oNearPoint->radius()."</td>";
886 echo "<td></td><td></td><td></td>";