6 * Operators describing special searches.
8 abstract class Operator
10 /// No operator selected.
12 /// Search for POI of the given type.
14 /// Search for POIs near the given place.
16 /// Search for POIS in the given place.
18 /// Search for POIS named as given.
20 /// Search for postcodes.
23 private $aConstantNames = null;
25 public static function toString($iOperator)
27 if ($iOperator == Operator::NONE) {
31 if ($aConstantNames === null) {
32 $oReflector = new \ReflectionClass ('Nominatim\Operator');
33 $aConstants = $oReflector->getConstants();
35 $aConstantNames = array();
36 foreach ($aConstants as $sName => $iValue) {
37 $aConstantNames[$iValue] = $sName;
41 return $aConstantNames[$iOperator];
46 * Description of a single interpretation of a search query.
48 class SearchDescription
50 /// Ranking how well the description fits the query.
51 private $iSearchRank = 0;
52 /// Country code of country the result must belong to.
53 private $sCountryCode = '';
54 /// List of word ids making up the name of the object.
55 private $aName = array();
56 /// List of word ids making up the address of the object.
57 private $aAddress = array();
58 /// Subset of word ids of full words making up the address.
59 private $aFullNameAddress = array();
60 /// List of word ids that appear in the name but should be ignored.
61 private $aNameNonSearch = array();
62 /// List of word ids that appear in the address but should be ignored.
63 private $aAddressNonSearch = array();
64 /// Kind of search for special searches, see Nominatim::Operator.
65 private $iOperator = Operator::NONE;
66 /// Class of special feature to search for.
68 /// Type of special feature to search for.
70 /// Housenumber of the object.
71 private $sHouseNumber = '';
72 /// Postcode for the object.
73 private $sPostcode = '';
74 /// Geographic search area.
75 private $oNearPoint = false;
77 // Temporary values used while creating the search description.
79 /// Index of phrase currently processed
80 private $iNamePhrase = -1;
82 public function getRank()
84 return $this->iSearchRank;
87 public function addToRank($iAddRank)
89 $this->iSearchRank += $iAddRank;
90 return $this->iSearchRank;
93 public function getPostCode()
95 return $this->sPostcode;
99 * Set the geographic search radius.
101 public function setNear(&$oNearPoint)
103 $this->oNearPoint = $oNearPoint;
106 public function setPoiSearch($iOperator, $sClass, $sType)
108 $this->iOperator = $iOperator;
109 $this->sClass = $sClass;
110 $this->sType = $sType;
114 * Check if name or address for the search are specified.
116 public function isNamedSearch()
118 return sizeof($this->aName) > 0 || sizeof($this->aAddress) > 0;
122 * Check if only a country is requested.
124 public function isCountrySearch()
126 return $this->sCountryCode && sizeof($this->aName) == 0
127 && !$this->iOperator && !$this->oNearPoint;
131 * Check if a search near a geographic location is requested.
133 public function isNearSearch()
135 return (bool) $this->oNearPoint;
138 public function isPoiSearch()
140 return (bool) $this->sClass;
143 public function looksLikeFullAddress()
145 return sizeof($this->aName)
146 && (sizeof($this->aAddress || $this->sCountryCode))
147 && preg_match('/[0-9]+/', $this->sHouseNumber);
150 public function isOperator($iType)
152 return $this->iOperator == $iType;
155 public function hasHouseNumber()
157 return (bool) $this->sHouseNumber;
160 private function poiTable()
162 return 'place_classtype_'.$this->sClass.'_'.$this->sType;
165 public function countryCodeSQL($sVar, $sCountryList)
167 if ($this->sCountryCode) {
168 return $sVar.' = \''.$this->sCountryCode."'";
171 return $sVar.' in ('.$sCountryList.')';
177 public function hasOperator()
179 return $this->iOperator != Operator::NONE;
183 * Extract special terms from the query, amend the search
184 * and return the shortended query.
186 * Only the first special term found will be used but all will
187 * be removed from the query.
189 public function extractKeyValuePairs($sQuery)
191 // Search for terms of kind [<key>=<value>].
193 '/\\[([\\w_]*)=([\\w_]*)\\]/',
199 foreach ($aSpecialTermsRaw as $aTerm) {
200 $sQuery = str_replace($aTerm[0], ' ', $sQuery);
201 if (!$this->hasOperator()) {
202 $this->setPoiSearch(Operator::TYPE, $aTerm[1], $aTerm[2]);
209 public function isValidSearch(&$aCountryCodes)
211 if (!sizeof($this->aName)) {
212 if ($this->sHouseNumber) {
217 && $this->sCountryCode
218 && !in_array($this->sCountryCode, $aCountryCodes)
226 /////////// Search building functions
228 public function extendWithFullTerm($aSearchTerm, $bWordInQuery, $bHasPartial, $sPhraseType, $bFirstToken, $bFirstPhrase, $bLastToken, &$iGlobalRank)
230 $aNewSearches = array();
232 if (($sPhraseType == '' || $sPhraseType == 'country')
233 && !empty($aSearchTerm['country_code'])
234 && $aSearchTerm['country_code'] != '0'
236 if (!$this->sCountryCode) {
237 $oSearch = clone $this;
238 $oSearch->iSearchRank++;
239 $oSearch->sCountryCode = $aSearchTerm['country_code'];
240 // Country is almost always at the end of the string
241 // - increase score for finding it anywhere else (optimisation)
243 $oSearch->iSearchRank += 5;
245 $aNewSearches[] = $oSearch;
247 // If it is at the beginning, we can be almost sure that
248 // the terms are in the wrong order. Increase score for all searches.
253 } elseif (($sPhraseType == '' || $sPhraseType == 'postalcode')
254 && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'postcode'
256 // We need to try the case where the postal code is the primary element
257 // (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode)
259 if (!$this->sPostcode && $bWordInQuery) {
260 // If we have structured search or this is the first term,
261 // make the postcode the primary search element.
262 if ($this->iOperator == Operator::NONE
263 && ($sPhraseType == 'postalcode' || $bFirstToken)
265 $oSearch = clone $this;
266 $oSearch->iSearchRank++;
267 $oSearch->iOperator = Operator::POSTCODE;
268 $oSearch->aAddress = array_merge($this->aAddress, $this->aName);
270 array($aSearchTerm['word_id'] => $aSearchTerm['word']);
271 $aNewSearches[] = $oSearch;
274 // If we have a structured search or this is not the first term,
275 // add the postcode as an addendum.
276 if ($this->iOperator != Operator::POSTCODE
277 && ($sPhraseType == 'postalcode' || sizeof($this->aName))
279 $oSearch = clone $this;
280 $oSearch->iSearchRank++;
281 $oSearch->sPostcode = $aSearchTerm['word'];
282 $aNewSearches[] = $oSearch;
285 } elseif (($sPhraseType == '' || $sPhraseType == 'street')
286 && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house'
288 if (!$this->sHouseNumber && $this->iOperator != Operator::POSTCODE) {
289 $oSearch = clone $this;
290 $oSearch->iSearchRank++;
291 $oSearch->sHouseNumber = trim($aSearchTerm['word_token']);
292 // sanity check: if the housenumber is not mainly made
293 // up of numbers, add a penalty
294 if (preg_match_all("/[^0-9]/", $oSearch->sHouseNumber, $aMatches) > 2) {
295 $oSearch->iSearchRank++;
297 // also must not appear in the middle of the address
298 if (sizeof($this->aAddress) || sizeof($this->aAddressNonSearch)) {
299 $oSearch->iSearchRank++;
301 $aNewSearches[] = $oSearch;
303 } elseif ($sPhraseType == ''
304 && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null
306 // require a normalized exact match of the term
307 // if we have the normalizer version of the query
309 if ($this->iOperator == Operator::NONE
310 && (isset($aSearchTerm['word']) && $aSearchTerm['word'])
313 $oSearch = clone $this;
314 $oSearch->iSearchRank++;
316 $iOp = Operator::NEAR; // near == in for the moment
317 if ($aSearchTerm['operator'] == '') {
318 if (sizeof($this->aName)) {
319 $iOp = Operator::NAME;
321 $oSearch->iSearchRank += 2;
324 $oSearch->setPoiSearch($iOp, $aSearchTerm['class'], $aSearchTerm['type']);
325 $aNewWordsetSearches[] = $oSearch;
327 } elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
328 $iWordID = $aSearchTerm['word_id'];
329 if (sizeof($this->aName)) {
330 if (($sPhraseType == '' || !$bFirstPhrase)
331 && $sPhraseType != 'country'
334 $oSearch = clone $this;
335 $oSearch->iSearchRank++;
336 $oSearch->aAddress[$iWordID] = $iWordID;
337 $aNewSearches[] = $oSearch;
340 $this->aFullNameAddress[$iWordID] = $iWordID;
343 $oSearch = clone $this;
344 $oSearch->iSearchRank++;
345 $oSearch->aName = array($iWordID => $iWordID);
346 $aNewSearches[] = $oSearch;
350 return $aNewSearches;
353 public function extendWithPartialTerm($aSearchTerm, $bStructuredPhrases, $iPhrase, &$aWordFrequencyScores, $aFullTokens)
355 // Only allow name terms.
356 if (!(isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])) {
360 $aNewSearches = array();
361 $iWordID = $aSearchTerm['word_id'];
363 if ((!$bStructuredPhrases || $iPhrase > 0)
364 && sizeof($this->aName)
365 && strpos($aSearchTerm['word_token'], ' ') === false
367 if ($aWordFrequencyScores[$iWordID] < CONST_Max_Word_Frequency) {
368 $oSearch = clone $this;
369 $oSearch->iSearchRank++;
370 $oSearch->aAddress[$iWordID] = $iWordID;
371 $aNewSearches[] = $oSearch;
373 $oSearch = clone $this;
374 $oSearch->iSearchRank++;
375 $oSearch->aAddressNonSearch[$iWordID] = $iWordID;
376 if (preg_match('#^[0-9]+$#', $aSearchTerm['word_token'])) {
377 $oSearch->iSearchRank += 2;
379 if (sizeof($aFullTokens)) {
380 $oSearch->iSearchRank++;
382 $aNewSearches[] = $oSearch;
384 // revert to the token version?
385 foreach ($aFullTokens as $aSearchTermToken) {
386 if (empty($aSearchTermToken['country_code'])
387 && empty($aSearchTermToken['lat'])
388 && empty($aSearchTermToken['class'])
390 $oSearch = clone $this;
391 $oSearch->iSearchRank++;
392 $oSearch->aAddress[$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
393 $aNewSearches[] = $oSearch;
399 if ((!$this->sPostcode && !$this->aAddress && !$this->aAddressNonSearch)
400 && (!sizeof($this->aName) || $this->iNamePhrase == $iPhrase)
402 $oSearch = clone $this;
403 $oSearch->iSearchRank++;
404 if (!sizeof($this->aName)) {
405 $oSearch->iSearchRank += 1;
407 if (preg_match('#^[0-9]+$#', $aSearchTerm['word_token'])) {
408 $oSearch->iSearchRank += 2;
410 if ($aWordFrequencyScores[$iWordID] < CONST_Max_Word_Frequency) {
411 $oSearch->aName[$iWordID] = $iWordID;
413 $oSearch->aNameNonSearch[$iWordID] = $iWordID;
415 $oSearch->iNamePhrase = $iPhrase;
416 $aNewSearches[] = $oSearch;
419 return $aNewSearches;
422 /////////// Query functions
424 public function queryCountry(&$oDB, $sViewboxSQL)
426 $sSQL = 'SELECT place_id FROM placex ';
427 $sSQL .= "WHERE country_code='".$this->sCountryCode."'";
428 $sSQL .= ' AND rank_search = 4';
430 $sSQL .= " AND ST_Intersects($sViewboxSQL, geometry)";
432 $sSQL .= " ORDER BY st_area(geometry) DESC LIMIT 1";
434 if (CONST_Debug) var_dump($sSQL);
436 return chksql($oDB->getCol($sSQL));
439 public function queryNearbyPoi(&$oDB, $sCountryList, $sViewboxSQL, $sViewboxCentreSQL, $sExcludeSQL, $iLimit)
441 if (!$this->sClass) {
445 $sPoiTable = $this->poiTable();
447 $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = \''.$sPoiTable."'";
448 if (chksql($oDB->getOne($sSQL))) {
449 $sSQL = 'SELECT place_id FROM '.$sPoiTable.' ct';
451 $sSQL .= ' JOIN placex USING (place_id)';
453 if ($this->oNearPoint) {
454 $sSQL .= ' WHERE '.$this->oNearPoint->withinSQL('ct.centroid');
456 $sSQL .= " WHERE ST_Contains($sViewboxSQL, ct.centroid)";
459 $sSQL .= " AND country_code in ($sCountryList)";
462 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
464 if ($sViewboxCentreSQL) {
465 $sSQL .= " ORDER BY ST_Distance($sViewboxCentreSQL, ct.centroid) ASC";
466 } elseif ($this->oNearPoint) {
467 $sSQL .= ' ORDER BY '.$this->oNearPoint->distanceSQL('ct.centroid').' ASC';
469 $sSQL .= " limit $iLimit";
470 if (CONST_Debug) var_dump($sSQL);
471 return chksql($oDB->getCol($sSQL));
474 if ($this->oNearPoint) {
475 $sSQL = 'SELECT place_id FROM placex WHERE ';
476 $sSQL .= 'class=\''.$this->sClass."' and type='".$this->sType."'";
477 $sSQL .= ' AND '.$this->oNearPoint->withinSQL('geometry');
478 $sSQL .= ' AND linked_place_id is null';
480 $sSQL .= " AND country_code in ($sCountryList)";
482 $sSQL .= ' ORDER BY '.$this->oNearPoint->distanceSQL('centroid')." ASC";
483 $sSQL .= " LIMIT $iLimit";
484 if (CONST_Debug) var_dump($sSQL);
485 return chksql($oDB->getCol($sSQL));
491 public function queryPostcode(&$oDB, $sCountryList, $iLimit)
493 $sSQL = 'SELECT p.place_id FROM location_postcode p ';
495 if (sizeof($this->aAddress)) {
496 $sSQL .= ', search_name s ';
497 $sSQL .= 'WHERE s.place_id = p.parent_place_id ';
498 $sSQL .= 'AND array_cat(s.nameaddress_vector, s.name_vector)';
499 $sSQL .= ' @> '.getArraySQL($this->aAddress).' AND ';
504 $sSQL .= "p.postcode = '".pg_escape_string(reset($this->$aName))."'";
505 $sCountryTerm = $this->countryCodeSQL('p.country_code', $sCountryList);
507 $sSQL .= ' AND '.$sCountyTerm;
509 $sSQL .= " LIMIT $iLimit";
511 if (CONST_Debug) var_dump($sSQL);
513 return chksql($oDB->getCol($sSQL));
516 public function queryNamedPlace(&$oDB, $aWordFrequencyScores, $sCountryList, $iMinAddressRank, $iMaxAddressRank, $sExcludeSQL, $sViewboxSmall, $sViewboxLarge, $iLimit)
521 if ($this->sHouseNumber && sizeof($this->aAddress)) {
522 $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
524 $aOrder[0] .= 'EXISTS(';
525 $aOrder[0] .= ' SELECT place_id';
526 $aOrder[0] .= ' FROM placex';
527 $aOrder[0] .= ' WHERE parent_place_id = search_name.place_id';
528 $aOrder[0] .= " AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
529 $aOrder[0] .= ' LIMIT 1';
531 // also housenumbers from interpolation lines table are needed
532 if (preg_match('/[0-9]+/', $this->sHouseNumber)) {
533 $iHouseNumber = intval($this->sHouseNumber);
534 $aOrder[0] .= 'OR EXISTS(';
535 $aOrder[0] .= ' SELECT place_id ';
536 $aOrder[0] .= ' FROM location_property_osmline ';
537 $aOrder[0] .= ' WHERE parent_place_id = search_name.place_id';
538 $aOrder[0] .= ' AND startnumber is not NULL';
539 $aOrder[0] .= ' AND '.$iHouseNumber.'>=startnumber ';
540 $aOrder[0] .= ' AND '.$iHouseNumber.'<=endnumber ';
541 $aOrder[0] .= ' LIMIT 1';
544 $aOrder[0] .= ') DESC';
547 if (sizeof($this->aName)) {
548 $aTerms[] = 'name_vector @> '.getArraySQL($this->aName);
550 if (sizeof($this->aAddress)) {
551 // For infrequent name terms disable index usage for address
552 if (CONST_Search_NameOnlySearchFrequencyThreshold
553 && sizeof($this->aName) == 1
554 && $aWordFrequencyScores[$this->aName[reset($this->aName)]]
555 < CONST_Search_NameOnlySearchFrequencyThreshold
557 $aTerms[] = 'array_cat(nameaddress_vector,ARRAY[]::integer[]) @> '.getArraySQL($this->aAddress);
559 $aTerms[] = 'nameaddress_vector @> '.getArraySQL($this->aAddress);
563 $sCountryTerm = $this->countryCodeSQL('country_code', $sCountryList);
565 $aTerms[] = $sCountryTerm;
568 if ($this->sHouseNumber) {
569 $aTerms[] = "address_rank between 16 and 27";
570 } elseif (!$this->sClass || $this->iOperator == Operator::NAME) {
571 if ($iMinAddressRank > 0) {
572 $aTerms[] = "address_rank >= ".$iMinAddressRank;
574 if ($iMaxAddressRank < 30) {
575 $aTerms[] = "address_rank <= ".$iMaxAddressRank;
579 if ($this->oNearPoint) {
580 $aTerms[] = $this->oNearPoint->withinSQL('centroid');
581 $aOrder[] = $this->oNearPoint->distanceSQL('centroid');
582 } elseif ($this->sPostcode) {
583 if (!sizeof($this->aAddress)) {
584 $aTerms[] = "EXISTS(SELECT place_id FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."' AND ST_DWithin(search_name.centroid, p.geometry, 0.1))";
586 $aOrder[] = "(SELECT min(ST_Distance(search_name.centroid, p.geometry)) FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."')";
591 $aTerms[] = 'place_id not in ('.$sExcludeSQL.')';
594 if ($sViewboxSmall) {
595 $aTerms[] = 'centroid && '.$sViewboxSmall;
598 if ($this->oNearPoint) {
599 $aOrder[] = $this->oNearPoint->distanceSQL('centroid');
602 if ($this->sHouseNumber) {
603 $sImportanceSQL = '- abs(26 - address_rank) + 3';
605 $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75-(search_rank::float/40) ELSE importance END)';
607 if ($sViewboxSmall) {
608 $sImportanceSQL .= " * CASE WHEN ST_Contains($sViewboxSmall, centroid) THEN 1 ELSE 0.5 END";
610 if ($sViewboxLarge) {
611 $sImportanceSQL .= " * CASE WHEN ST_Contains($sViewboxLarge, centroid) THEN 1 ELSE 0.5 END";
613 $aOrder[] = "$sImportanceSQL DESC";
615 if (sizeof($this->aFullNameAddress)) {
616 $sExactMatchSQL = ' ( ';
617 $sExactMatchSQL .= ' SELECT count(*) FROM ( ';
618 $sExactMatchSQL .= ' SELECT unnest('.getArraySQL($this->aFullNameAddress).')';
619 $sExactMatchSQL .= ' INTERSECT ';
620 $sExactMatchSQL .= ' SELECT unnest(nameaddress_vector)';
621 $sExactMatchSQL .= ' ) s';
622 $sExactMatchSQL .= ') as exactmatch';
623 $aOrder[] = 'exactmatch DESC';
625 $sExactMatchSQL = '0::int as exactmatch';
628 if ($this->sHouseNumber || $this->sClass) {
632 if (sizeof($aTerms)) {
633 $sSQL = 'SELECT place_id,'.$sExactMatchSQL;
634 $sSQL .= ' FROM search_name';
635 $sSQL .= ' WHERE '.join(' and ', $aTerms);
636 $sSQL .= ' ORDER BY '.join(', ', $aOrder);
637 $sSQL .= ' LIMIT '.$iLimit;
639 if (CONST_Debug) var_dump($sSQL);
643 "Could not get places for search terms."
651 public function queryHouseNumber(&$oDB, $aRoadPlaceIDs, $sExcludeSQL, $iLimit)
653 $sPlaceIDs = join(',', $aRoadPlaceIDs);
655 $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
656 $sSQL = 'SELECT place_id FROM placex ';
657 $sSQL .= 'WHERE parent_place_id in ('.$sPlaceIDs.')';
658 $sSQL .= " AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
660 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
662 $sSQL .= " LIMIT $iLimit";
664 if (CONST_Debug) var_dump($sSQL);
666 $aPlaceIDs = chksql($oDB->getCol($sSQL));
668 if (sizeof($aPlaceIDs)) {
669 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
672 $bIsIntHouseNumber= (bool) preg_match('/[0-9]+/', $this->sHouseNumber);
673 $iHousenumber = intval($this->sHouseNumber);
674 if ($bIsIntHouseNumber) {
675 // if nothing found, search in the interpolation line table
676 $sSQL = 'SELECT distinct place_id FROM location_property_osmline';
677 $sSQL .= ' WHERE startnumber is not NULL';
678 $sSQL .= ' AND parent_place_id in ('.$sPlaceIDs.') AND (';
679 if ($iHousenumber % 2 == 0) {
680 // If housenumber is even, look for housenumber in streets
681 // with interpolationtype even or all.
682 $sSQL .= "interpolationtype='even'";
684 // Else look for housenumber with interpolationtype odd or all.
685 $sSQL .= "interpolationtype='odd'";
687 $sSQL .= " or interpolationtype='all') and ";
688 $sSQL .= $iHousenumber.">=startnumber and ";
689 $sSQL .= $iHousenumber."<=endnumber";
692 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
694 $sSQL .= " limit $iLimit";
696 if (CONST_Debug) var_dump($sSQL);
698 $aPlaceIDs = chksql($oDB->getCol($sSQL, 0));
700 if (sizeof($aPlaceIDs)) {
701 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
705 // If nothing found try the aux fallback table
706 if (CONST_Use_Aux_Location_data) {
707 $sSQL = 'SELECT place_id FROM location_property_aux';
708 $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.')';
709 $sSQL .= " AND housenumber = '".$this->sHouseNumber."'";
711 $sSQL .= " AND place_id not in ($sExcludeSQL)";
713 $sSQL .= " limit $iLimit";
715 if (CONST_Debug) var_dump($sSQL);
717 $aPlaceIDs = chksql($oDB->getCol($sSQL));
719 if (sizeof($aPlaceIDs)) {
720 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
724 // If nothing found then search in Tiger data (location_property_tiger)
725 if (CONST_Use_US_Tiger_Data && $bIsIntHouseNumber) {
726 $sSQL = 'SELECT distinct place_id FROM location_property_tiger';
727 $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.') and (';
728 if ($iHousenumber % 2 == 0) {
729 $sSQL .= "interpolationtype='even'";
731 $sSQL .= "interpolationtype='odd'";
733 $sSQL .= " or interpolationtype='all') and ";
734 $sSQL .= $iHousenumber.">=startnumber and ";
735 $sSQL .= $iHousenumber."<=endnumber";
738 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
740 $sSQL .= " limit $iLimit";
742 if (CONST_Debug) var_dump($sSQL);
744 $aPlaceIDs = chksql($oDB->getCol($sSQL, 0));
746 if (sizeof($aPlaceIDs)) {
747 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
755 public function queryPoiByOperator(&$oDB, $aParentIDs, $sExcludeSQL, $iLimit)
757 $sPlaceIDs = join(',', $aParentIDs);
758 $aClassPlaceIDs = array();
760 if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NAME) {
761 // If they were searching for a named class (i.e. 'Kings Head pub')
762 // then we might have an extra match
763 $sSQL = 'SELECT place_id FROM placex ';
764 $sSQL .= " WHERE place_id in ($sPlaceIDs)";
765 $sSQL .= " AND class='".$this->sClass."' ";
766 $sSQL .= " AND type='".$this->sType."'";
767 $sSQL .= " AND linked_place_id is null";
768 $sSQL .= " ORDER BY rank_search ASC ";
769 $sSQL .= " LIMIT $iLimit";
771 if (CONST_Debug) var_dump($sSQL);
773 $aClassPlaceIDs = chksql($oDB->getCol($sSQL));
776 // NEAR and IN are handled the same
777 if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NEAR) {
778 $sClassTable = $this->poiTable();
779 $sSQL = "SELECT count(*) FROM pg_tables WHERE tablename = '$sClassTable'";
780 $bCacheTable = (bool) chksql($oDB->getOne($sSQL));
782 $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)";
783 if (CONST_Debug) var_dump($sSQL);
784 $iMaxRank = (int)chksql($oDB->getOne($sSQL));
786 // For state / country level searches the normal radius search doesn't work very well
788 if ($iMaxRank < 9 && $bCacheTable) {
789 // Try and get a polygon to search in instead
790 $sSQL = 'SELECT geometry FROM placex';
791 $sSQL .= " WHERE place_id in ($sPlaceIDs)";
792 $sSQL .= " AND rank_search < $iMaxRank + 5";
793 $sSQL .= " AND ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon')";
794 $sSQL .= " ORDER BY rank_search ASC ";
796 if (CONST_Debug) var_dump($sSQL);
797 $sPlaceGeom = chksql($oDB->getOne($sSQL));
804 $sSQL = 'SELECT place_id FROM placex';
805 $sSQL .= " WHERE place_id in ($sPlaceIDs) and rank_search < $iMaxRank";
806 if (CONST_Debug) var_dump($sSQL);
807 $aPlaceIDs = chksql($oDB->getCol($sSQL));
808 $sPlaceIDs = join(',', $aPlaceIDs);
811 if ($sPlaceIDs || $sPlaceGeom) {
814 // More efficient - can make the range bigger
818 if ($this->oNearPoint) {
819 $sOrderBySQL = $this->oNearPoint->distanceSQL('l.centroid');
820 } elseif ($sPlaceIDs) {
821 $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
822 } elseif ($sPlaceGeom) {
823 $sOrderBySQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
826 $sSQL = 'SELECT distinct i.place_id';
828 $sSQL .= ', i.order_term';
830 $sSQL .= ' from (SELECT l.place_id';
832 $sSQL .= ','.$sOrderBySQL.' as order_term';
834 $sSQL .= ' from '.$sClassTable.' as l';
837 $sSQL .= ",placex as f WHERE ";
838 $sSQL .= "f.place_id in ($sPlaceIDs) ";
839 $sSQL .= " AND ST_DWithin(l.centroid, f.centroid, $fRange)";
840 } elseif ($sPlaceGeom) {
841 $sSQL .= " WHERE ST_Contains('$sPlaceGeom', l.centroid)";
845 $sSQL .= ' AND l.place_id not in ('.$sExcludeSQL.')';
847 $sSQL .= 'limit 300) i ';
849 $sSQL .= 'order by order_term asc';
851 $sSQL .= " limit $iLimit";
853 if (CONST_Debug) var_dump($sSQL);
855 $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($oDB->getCol($sSQL)));
857 if ($this->oNearPoint) {
858 $fRange = $this->oNearPoint->radius();
862 if ($this->oNearPoint) {
863 $sOrderBySQL = $this->oNearPoint->distanceSQL('l.geometry');
865 $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
868 $sSQL = 'SELECT distinct l.place_id';
870 $sSQL .= ','.$sOrderBySQL.' as orderterm';
872 $sSQL .= ' FROM placex as l, placex as f';
873 $sSQL .= " WHERE f.place_id in ($sPlaceIDs)";
874 $sSQL .= " AND ST_DWithin(l.geometry, f.centroid, $fRange)";
875 $sSQL .= " AND l.class='".$this->sClass."'";
876 $sSQL .= " AND l.type='".$this->sType."'";
878 $sSQL .= " AND l.place_id not in (".$sExcludeSQL.")";
881 $sSQL .= "ORDER BY orderterm ASC";
883 $sSQL .= " limit $iLimit";
885 if (CONST_Debug) var_dump($sSQL);
887 $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($oDB->getCol($sSQL)));
892 return $aClassPlaceIDs;
896 /////////// Sort functions
898 static function bySearchRank($a, $b)
900 if ($a->iSearchRank == $b->iSearchRank) {
901 return $a->iOperator + strlen($a->sHouseNumber)
902 - $b->iOperator - strlen($b->sHouseNumber);
905 return $a->iSearchRank < $b->iSearchRank ? -1 : 1;
908 //////////// Debugging functions
910 function dumpAsHtmlTableRow(&$aWordIDs)
912 $kf = function($k) use (&$aWordIDs) { return $aWordIDs[$k]; };
915 echo "<td>$this->iSearchRank</td>";
916 echo "<td>".join(', ', array_map($kf, $this->aName))."</td>";
917 echo "<td>".join(', ', array_map($kf, $this->aNameNonSearch))."</td>";
918 echo "<td>".join(', ', array_map($kf, $this->aAddress))."</td>";
919 echo "<td>".join(', ', array_map($kf, $this->aAddressNonSearch))."</td>";
920 echo "<td>".$this->sCountryCode."</td>";
921 echo "<td>".Operator::toString($this->iOperator)."</td>";
922 echo "<td>".$this->sClass."</td>";
923 echo "<td>".$this->sType."</td>";
924 echo "<td>".$this->sPostcode."</td>";
925 echo "<td>".$this->sHouseNumber."</td>";
927 if ($this->oNearPoint) {
928 echo "<td>".$this->oNearPoint->lat()."</td>";
929 echo "<td>".$this->oNearPoint->lon()."</td>";
930 echo "<td>".$this->oNearPoint->radius()."</td>";
932 echo "<td></td><td></td><td></td>";