5 require_once(CONST_BasePath.'/lib/SpecialSearchOperator.php');
6 require_once(CONST_BasePath.'/lib/SearchContext.php');
9 * Description of a single interpretation of a search query.
11 class SearchDescription
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.
31 /// Type of special feature to search for.
33 /// Housenumber of the object.
34 private $sHouseNumber = '';
35 /// Postcode for the object.
36 private $sPostcode = '';
37 /// Global search constraints.
40 // Temporary values used while creating the search description.
42 /// Index of phrase currently processed.
43 private $iNamePhrase = -1;
46 public function __construct($oContext)
48 $this->oContext = $oContext;
51 public function getRank()
53 return $this->iSearchRank;
56 public function addToRank($iAddRank)
58 $this->iSearchRank += $iAddRank;
59 return $this->iSearchRank;
62 public function setPoiSearch($iOperator, $sClass, $sType)
64 $this->iOperator = $iOperator;
65 $this->sClass = $sClass;
66 $this->sType = $sType;
69 public function looksLikeFullAddress()
71 return sizeof($this->aName)
72 && (sizeof($this->aAddress || $this->sCountryCode))
73 && preg_match('/[0-9]+/', $this->sHouseNumber);
76 private function poiTable()
78 return 'place_classtype_'.$this->sClass.'_'.$this->sType;
81 public function countryCodeSQL($sVar)
83 if ($this->sCountryCode) {
84 return $sVar.' = \''.$this->sCountryCode."'";
86 if ($this->oContext->sqlCountryList) {
87 return $sVar.' in '.$this->oContext->sqlCountryList;
93 public function hasOperator()
95 return $this->iOperator != Operator::NONE;
98 public function extractKeyValuePairs($sQuery)
100 // Search for terms of kind [<key>=<value>].
102 '/\\[([\\w_]*)=([\\w_]*)\\]/',
108 foreach ($aSpecialTermsRaw as $aTerm) {
109 $sQuery = str_replace($aTerm[0], ' ', $sQuery);
110 if (!$this->hasOperator()) {
111 $this->setPoiSearch(Operator::TYPE, $aTerm[1], $aTerm[2]);
118 public function isValidSearch(&$aCountryCodes)
120 if (!sizeof($this->aName)) {
121 if ($this->sHouseNumber) {
126 && $this->sCountryCode
127 && !in_array($this->sCountryCode, $aCountryCodes)
135 /////////// Search building functions
138 public function extendWithFullTerm($aSearchTerm, $bWordInQuery, $bHasPartial, $sPhraseType, $bFirstToken, $bFirstPhrase, $bLastToken, &$iGlobalRank)
140 $aNewSearches = array();
142 if (($sPhraseType == '' || $sPhraseType == 'country')
143 && !empty($aSearchTerm['country_code'])
144 && $aSearchTerm['country_code'] != '0'
146 if (!$this->sCountryCode) {
147 $oSearch = clone $this;
148 $oSearch->iSearchRank++;
149 $oSearch->sCountryCode = $aSearchTerm['country_code'];
150 // Country is almost always at the end of the string
151 // - increase score for finding it anywhere else (optimisation)
153 $oSearch->iSearchRank += 5;
155 $aNewSearches[] = $oSearch;
157 // If it is at the beginning, we can be almost sure that
158 // the terms are in the wrong order. Increase score for all searches.
163 } elseif (($sPhraseType == '' || $sPhraseType == 'postalcode')
164 && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'postcode'
166 // We need to try the case where the postal code is the primary element
167 // (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode)
169 if (!$this->sPostcode && $bWordInQuery
170 && pg_escape_string($aSearchTerm['word']) == $aSearchTerm['word']
172 // If we have structured search or this is the first term,
173 // make the postcode the primary search element.
174 if ($this->iOperator == Operator::NONE
175 && ($sPhraseType == 'postalcode' || $bFirstToken)
177 $oSearch = clone $this;
178 $oSearch->iSearchRank++;
179 $oSearch->iOperator = Operator::POSTCODE;
180 $oSearch->aAddress = array_merge($this->aAddress, $this->aName);
182 array($aSearchTerm['word_id'] => $aSearchTerm['word']);
183 $aNewSearches[] = $oSearch;
186 // If we have a structured search or this is not the first term,
187 // add the postcode as an addendum.
188 if ($this->iOperator != Operator::POSTCODE
189 && ($sPhraseType == 'postalcode' || sizeof($this->aName))
191 $oSearch = clone $this;
192 $oSearch->iSearchRank++;
193 $oSearch->sPostcode = $aSearchTerm['word'];
194 $aNewSearches[] = $oSearch;
197 } elseif (($sPhraseType == '' || $sPhraseType == 'street')
198 && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house'
200 if (!$this->sHouseNumber && $this->iOperator != Operator::POSTCODE) {
201 $oSearch = clone $this;
202 $oSearch->iSearchRank++;
203 $oSearch->sHouseNumber = trim($aSearchTerm['word_token']);
204 // sanity check: if the housenumber is not mainly made
205 // up of numbers, add a penalty
206 if (preg_match_all("/[^0-9]/", $oSearch->sHouseNumber, $aMatches) > 2) {
207 $oSearch->iSearchRank++;
209 if (!isset($aSearchTerm['word_id'])) {
210 $oSearch->iSearchRank++;
212 // also must not appear in the middle of the address
213 if (sizeof($this->aAddress) || sizeof($this->aAddressNonSearch)) {
214 $oSearch->iSearchRank++;
216 $aNewSearches[] = $oSearch;
218 } elseif ($sPhraseType == ''
219 && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null
221 // require a normalized exact match of the term
222 // if we have the normalizer version of the query
224 if ($this->iOperator == Operator::NONE
225 && (isset($aSearchTerm['word']) && $aSearchTerm['word'])
228 $oSearch = clone $this;
229 $oSearch->iSearchRank++;
231 $iOp = Operator::NEAR; // near == in for the moment
232 if ($aSearchTerm['operator'] == '') {
233 if (sizeof($this->aName)) {
234 $iOp = Operator::NAME;
236 $oSearch->iSearchRank += 2;
239 $oSearch->setPoiSearch($iOp, $aSearchTerm['class'], $aSearchTerm['type']);
240 $aNewSearches[] = $oSearch;
242 } elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
243 $iWordID = $aSearchTerm['word_id'];
244 if (sizeof($this->aName)) {
245 if (($sPhraseType == '' || !$bFirstPhrase)
246 && $sPhraseType != 'country'
249 $oSearch = clone $this;
250 $oSearch->iSearchRank++;
251 $oSearch->aAddress[$iWordID] = $iWordID;
252 $aNewSearches[] = $oSearch;
254 $this->aFullNameAddress[$iWordID] = $iWordID;
257 $oSearch = clone $this;
258 $oSearch->iSearchRank++;
259 $oSearch->aName = array($iWordID => $iWordID);
260 $aNewSearches[] = $oSearch;
264 return $aNewSearches;
267 public function extendWithPartialTerm($aSearchTerm, $bStructuredPhrases, $iPhrase, &$aWordFrequencyScores, $aFullTokens)
269 // Only allow name terms.
270 if (!(isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])) {
274 $aNewSearches = array();
275 $iWordID = $aSearchTerm['word_id'];
277 if ((!$bStructuredPhrases || $iPhrase > 0)
278 && sizeof($this->aName)
279 && strpos($aSearchTerm['word_token'], ' ') === false
281 if ($aWordFrequencyScores[$iWordID] < CONST_Max_Word_Frequency) {
282 $oSearch = clone $this;
283 $oSearch->iSearchRank++;
284 $oSearch->aAddress[$iWordID] = $iWordID;
285 $aNewSearches[] = $oSearch;
287 $oSearch = clone $this;
288 $oSearch->iSearchRank++;
289 $oSearch->aAddressNonSearch[$iWordID] = $iWordID;
290 if (preg_match('#^[0-9]+$#', $aSearchTerm['word_token'])) {
291 $oSearch->iSearchRank += 2;
293 if (sizeof($aFullTokens)) {
294 $oSearch->iSearchRank++;
296 $aNewSearches[] = $oSearch;
298 // revert to the token version?
299 foreach ($aFullTokens as $aSearchTermToken) {
300 if (empty($aSearchTermToken['country_code'])
301 && empty($aSearchTermToken['lat'])
302 && empty($aSearchTermToken['class'])
304 $oSearch = clone $this;
305 $oSearch->iSearchRank++;
306 $oSearch->aAddress[$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
307 $aNewSearches[] = $oSearch;
313 if ((!$this->sPostcode && !$this->aAddress && !$this->aAddressNonSearch)
314 && (!sizeof($this->aName) || $this->iNamePhrase == $iPhrase)
316 $oSearch = clone $this;
317 $oSearch->iSearchRank++;
318 if (!sizeof($this->aName)) {
319 $oSearch->iSearchRank += 1;
321 if (preg_match('#^[0-9]+$#', $aSearchTerm['word_token'])) {
322 $oSearch->iSearchRank += 2;
324 if ($aWordFrequencyScores[$iWordID] < CONST_Max_Word_Frequency) {
325 $oSearch->aName[$iWordID] = $iWordID;
327 $oSearch->aNameNonSearch[$iWordID] = $iWordID;
329 $oSearch->iNamePhrase = $iPhrase;
330 $aNewSearches[] = $oSearch;
333 return $aNewSearches;
336 /////////// Query functions
338 public function query(&$oDB, &$aWordFrequencyScores, &$aExactMatchCache, $iMinRank, $iMaxRank, $iLimit)
340 $aPlaceIDs = array();
343 if ($this->sCountryCode
344 && !sizeof($this->aName)
347 && !$this->oContext->hasNearPoint()
349 // Just looking for a country - look it up
350 if (4 >= $iMinRank && 4 <= $iMaxRank) {
351 $aPlaceIDs = $this->queryCountry($oDB);
353 } elseif (!sizeof($this->aName) && !sizeof($this->aAddress)) {
354 // Neither name nor address? Then we must be
355 // looking for a POI in a geographic area.
356 if ($this->oContext->isBoundedSearch()) {
357 $aPlaceIDs = $this->queryNearbyPoi($oDB, $iLimit);
359 } elseif ($this->iOperator == Operator::POSTCODE) {
360 // looking for postcode
361 $aPlaceIDs = $this->queryPostcode($oDB, $iLimit);
364 // First search for places according to name and address.
365 $aNamedPlaceIDs = $this->queryNamedPlace(
367 $aWordFrequencyScores,
373 if (sizeof($aNamedPlaceIDs)) {
374 foreach ($aNamedPlaceIDs as $aRow) {
375 $aPlaceIDs[] = $aRow['place_id'];
376 $aExactMatchCache[$aRow['place_id']] = $aRow['exactmatch'];
380 //now search for housenumber, if housenumber provided
381 if ($this->sHouseNumber && sizeof($aPlaceIDs)) {
382 $aResult = $this->queryHouseNumber($oDB, $aPlaceIDs, $iLimit);
384 if (sizeof($aResult)) {
385 $iHousenumber = $aResult['iHouseNumber'];
386 $aPlaceIDs = $aResult['aPlaceIDs'];
387 } elseif (!$this->looksLikeFullAddress()) {
388 $aPlaceIDs = array();
392 // finally get POIs if requested
393 if ($this->sClass && sizeof($aPlaceIDs)) {
394 $aPlaceIDs = $this->queryPoiByOperator($oDB, $aPlaceIDs, $iLimit);
399 echo "<br><b>Place IDs:</b> ";
400 var_Dump($aPlaceIDs);
403 if (sizeof($aPlaceIDs) && $this->sPostcode) {
404 $sSQL = 'SELECT place_id FROM placex';
405 $sSQL .= ' WHERE place_id in ('.join(',', $aPlaceIDs).')';
406 $sSQL .= " AND postcode = '".$this->sPostcode."'";
407 if (CONST_Debug) var_dump($sSQL);
408 $aFilteredPlaceIDs = chksql($oDB->getCol($sSQL));
409 if ($aFilteredPlaceIDs) {
410 $aPlaceIDs = $aFilteredPlaceIDs;
412 echo "<br><b>Place IDs after postcode filtering:</b> ";
413 var_Dump($aPlaceIDs);
418 return array('IDs' => $aPlaceIDs, 'houseNumber' => $iHousenumber);
422 private function queryCountry(&$oDB)
424 $sSQL = 'SELECT place_id FROM placex ';
425 $sSQL .= "WHERE country_code='".$this->sCountryCode."'";
426 $sSQL .= ' AND rank_search = 4';
427 if ($this->oContext->bViewboxBounded) {
428 $sSQL .= ' AND ST_Intersects('.$this->oContext->sqlViewboxSmall.', geometry)';
430 $sSQL .= " ORDER BY st_area(geometry) DESC LIMIT 1";
432 if (CONST_Debug) var_dump($sSQL);
434 return chksql($oDB->getCol($sSQL));
437 private function queryNearbyPoi(&$oDB, $iLimit)
439 if (!$this->sClass) {
443 $sPoiTable = $this->poiTable();
445 $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = \''.$sPoiTable."'";
446 if (chksql($oDB->getOne($sSQL))) {
447 $sSQL = 'SELECT place_id FROM '.$sPoiTable.' ct';
448 if ($this->oContext->sqlCountryList) {
449 $sSQL .= ' JOIN placex USING (place_id)';
451 if ($this->oContext->hasNearPoint()) {
452 $sSQL .= ' WHERE '.$this->oContext->withinSQL('ct.centroid');
453 } else if ($this->oContext->bViewboxBounded) {
454 $sSQL .= ' WHERE ST_Contains('.$this->oContext->sqlViewboxSmall.', ct.centroid)';
456 if ($this->oContext->sqlCountryList) {
457 $sSQL .= ' AND country_code in '.$this->oContext->sqlCountryList;
459 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
460 if ($this->oContext->sqlViewboxCentre) {
461 $sSQL .= ' ORDER BY ST_Distance(';
462 $sSQL .= $this->oContext->sqlViewboxCentre.', ct.centroid) ASC';
463 } elseif ($this->oContext->hasNearPoint()) {
464 $sSQL .= ' ORDER BY '.$this->oContext->distanceSQL('ct.centroid').' ASC';
466 $sSQL .= " limit $iLimit";
467 if (CONST_Debug) var_dump($sSQL);
468 return chksql($oDB->getCol($sSQL));
471 if ($this->oContext->hasNearPoint()) {
472 $sSQL = 'SELECT place_id FROM placex WHERE ';
473 $sSQL .= 'class=\''.$this->sClass."' and type='".$this->sType."'";
474 $sSQL .= ' AND '.$this->oContext->withinSQL('geometry');
475 $sSQL .= ' AND linked_place_id is null';
476 if ($this->oContext->sqlCountryList) {
477 $sSQL .= ' AND country_code in '.$this->oContext->sqlCountryList;
479 $sSQL .= ' ORDER BY '.$this->oContext->distanceSQL('centroid')." ASC";
480 $sSQL .= " LIMIT $iLimit";
481 if (CONST_Debug) var_dump($sSQL);
482 return chksql($oDB->getCol($sSQL));
488 private function queryPostcode(&$oDB, $iLimit)
490 $sSQL = 'SELECT p.place_id FROM location_postcode p ';
492 if (sizeof($this->aAddress)) {
493 $sSQL .= ', search_name s ';
494 $sSQL .= 'WHERE s.place_id = p.parent_place_id ';
495 $sSQL .= 'AND array_cat(s.nameaddress_vector, s.name_vector)';
496 $sSQL .= ' @> '.getArraySQL($this->aAddress).' AND ';
501 $sSQL .= "p.postcode = '".reset($this->aName)."'";
502 $sSQL .= $this->countryCodeSQL(' AND p.country_code');
503 $sSQL .= $this->oContext->excludeSQL(' AND p.place_id');
504 $sSQL .= " LIMIT $iLimit";
506 if (CONST_Debug) var_dump($sSQL);
508 return chksql($oDB->getCol($sSQL));
511 private function queryNamedPlace(&$oDB, $aWordFrequencyScores, $iMinAddressRank, $iMaxAddressRank, $iLimit)
516 if ($this->sHouseNumber && sizeof($this->aAddress)) {
517 $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
519 $aOrder[0] .= 'EXISTS(';
520 $aOrder[0] .= ' SELECT place_id';
521 $aOrder[0] .= ' FROM placex';
522 $aOrder[0] .= ' WHERE parent_place_id = search_name.place_id';
523 $aOrder[0] .= " AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
524 $aOrder[0] .= ' LIMIT 1';
526 // also housenumbers from interpolation lines table are needed
527 if (preg_match('/[0-9]+/', $this->sHouseNumber)) {
528 $iHouseNumber = intval($this->sHouseNumber);
529 $aOrder[0] .= 'OR EXISTS(';
530 $aOrder[0] .= ' SELECT place_id ';
531 $aOrder[0] .= ' FROM location_property_osmline ';
532 $aOrder[0] .= ' WHERE parent_place_id = search_name.place_id';
533 $aOrder[0] .= ' AND startnumber is not NULL';
534 $aOrder[0] .= ' AND '.$iHouseNumber.'>=startnumber ';
535 $aOrder[0] .= ' AND '.$iHouseNumber.'<=endnumber ';
536 $aOrder[0] .= ' LIMIT 1';
539 $aOrder[0] .= ') DESC';
542 if (sizeof($this->aName)) {
543 $aTerms[] = 'name_vector @> '.getArraySQL($this->aName);
545 if (sizeof($this->aAddress)) {
546 // For infrequent name terms disable index usage for address
547 if (CONST_Search_NameOnlySearchFrequencyThreshold
548 && sizeof($this->aName) == 1
549 && $aWordFrequencyScores[$this->aName[reset($this->aName)]]
550 < CONST_Search_NameOnlySearchFrequencyThreshold
552 $aTerms[] = 'array_cat(nameaddress_vector,ARRAY[]::integer[]) @> '.getArraySQL($this->aAddress);
554 $aTerms[] = 'nameaddress_vector @> '.getArraySQL($this->aAddress);
558 $sCountryTerm = $this->countryCodeSQL('country_code');
560 $aTerms[] = $sCountryTerm;
563 if ($this->sHouseNumber) {
564 $aTerms[] = "address_rank between 16 and 27";
565 } elseif (!$this->sClass || $this->iOperator == Operator::NAME) {
566 if ($iMinAddressRank > 0) {
567 $aTerms[] = "address_rank >= ".$iMinAddressRank;
569 if ($iMaxAddressRank < 30) {
570 $aTerms[] = "address_rank <= ".$iMaxAddressRank;
574 if ($this->oContext->hasNearPoint()) {
575 $aTerms[] = $this->oContext->withinSQL('centroid');
576 $aOrder[] = $this->oContext->distanceSQL('centroid');
577 } elseif ($this->sPostcode) {
578 if (!sizeof($this->aAddress)) {
579 $aTerms[] = "EXISTS(SELECT place_id FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."' AND ST_DWithin(search_name.centroid, p.geometry, 0.1))";
581 $aOrder[] = "(SELECT min(ST_Distance(search_name.centroid, p.geometry)) FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."')";
585 $sExcludeSQL = $this->oContext->excludeSQL('place_id');
587 $aTerms[] = $sExcludeSQL;
590 if ($this->oContext->bViewboxBounded) {
591 $aTerms[] = 'centroid && '.$this->oContext->sqlViewboxSmall;
594 if ($this->oContext->hasNearPoint()) {
595 $aOrder[] = $this->oContext->distanceSQL('centroid');
598 if ($this->sHouseNumber) {
599 $sImportanceSQL = '- abs(26 - address_rank) + 3';
601 $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75-(search_rank::float/40) ELSE importance END)';
603 $sImportanceSQL .= $this->oContext->viewboxImportanceSQL('centroid');
604 $aOrder[] = "$sImportanceSQL DESC";
606 if (sizeof($this->aFullNameAddress)) {
607 $sExactMatchSQL = ' ( ';
608 $sExactMatchSQL .= ' SELECT count(*) FROM ( ';
609 $sExactMatchSQL .= ' SELECT unnest('.getArraySQL($this->aFullNameAddress).')';
610 $sExactMatchSQL .= ' INTERSECT ';
611 $sExactMatchSQL .= ' SELECT unnest(nameaddress_vector)';
612 $sExactMatchSQL .= ' ) s';
613 $sExactMatchSQL .= ') as exactmatch';
614 $aOrder[] = 'exactmatch DESC';
616 $sExactMatchSQL = '0::int as exactmatch';
619 if ($this->sHouseNumber || $this->sClass) {
623 if (sizeof($aTerms)) {
624 $sSQL = 'SELECT place_id,'.$sExactMatchSQL;
625 $sSQL .= ' FROM search_name';
626 $sSQL .= ' WHERE '.join(' and ', $aTerms);
627 $sSQL .= ' ORDER BY '.join(', ', $aOrder);
628 $sSQL .= ' LIMIT '.$iLimit;
630 if (CONST_Debug) var_dump($sSQL);
634 "Could not get places for search terms."
641 private function queryHouseNumber(&$oDB, $aRoadPlaceIDs, $iLimit)
643 $sPlaceIDs = join(',', $aRoadPlaceIDs);
645 $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
646 $sSQL = 'SELECT place_id FROM placex ';
647 $sSQL .= 'WHERE parent_place_id in ('.$sPlaceIDs.')';
648 $sSQL .= " AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
649 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
650 $sSQL .= " LIMIT $iLimit";
652 if (CONST_Debug) var_dump($sSQL);
654 $aPlaceIDs = chksql($oDB->getCol($sSQL));
656 if (sizeof($aPlaceIDs)) {
657 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
660 $bIsIntHouseNumber= (bool) preg_match('/[0-9]+/', $this->sHouseNumber);
661 $iHousenumber = intval($this->sHouseNumber);
662 if ($bIsIntHouseNumber) {
663 // if nothing found, search in the interpolation line table
664 $sSQL = 'SELECT distinct place_id FROM location_property_osmline';
665 $sSQL .= ' WHERE startnumber is not NULL';
666 $sSQL .= ' AND parent_place_id in ('.$sPlaceIDs.') AND (';
667 if ($iHousenumber % 2 == 0) {
668 // If housenumber is even, look for housenumber in streets
669 // with interpolationtype even or all.
670 $sSQL .= "interpolationtype='even'";
672 // Else look for housenumber with interpolationtype odd or all.
673 $sSQL .= "interpolationtype='odd'";
675 $sSQL .= " or interpolationtype='all') and ";
676 $sSQL .= $iHousenumber.">=startnumber and ";
677 $sSQL .= $iHousenumber."<=endnumber";
678 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
679 $sSQL .= " limit $iLimit";
681 if (CONST_Debug) var_dump($sSQL);
683 $aPlaceIDs = chksql($oDB->getCol($sSQL, 0));
685 if (sizeof($aPlaceIDs)) {
686 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
690 // If nothing found try the aux fallback table
691 if (CONST_Use_Aux_Location_data) {
692 $sSQL = 'SELECT place_id FROM location_property_aux';
693 $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.')';
694 $sSQL .= " AND housenumber = '".$this->sHouseNumber."'";
695 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
696 $sSQL .= " limit $iLimit";
698 if (CONST_Debug) var_dump($sSQL);
700 $aPlaceIDs = chksql($oDB->getCol($sSQL));
702 if (sizeof($aPlaceIDs)) {
703 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
707 // If nothing found then search in Tiger data (location_property_tiger)
708 if (CONST_Use_US_Tiger_Data && $bIsIntHouseNumber) {
709 $sSQL = 'SELECT distinct place_id FROM location_property_tiger';
710 $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.') and (';
711 if ($iHousenumber % 2 == 0) {
712 $sSQL .= "interpolationtype='even'";
714 $sSQL .= "interpolationtype='odd'";
716 $sSQL .= " or interpolationtype='all') and ";
717 $sSQL .= $iHousenumber.">=startnumber and ";
718 $sSQL .= $iHousenumber."<=endnumber";
719 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
720 $sSQL .= " limit $iLimit";
722 if (CONST_Debug) var_dump($sSQL);
724 $aPlaceIDs = chksql($oDB->getCol($sSQL, 0));
726 if (sizeof($aPlaceIDs)) {
727 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
735 private function queryPoiByOperator(&$oDB, $aParentIDs, $iLimit)
737 $sPlaceIDs = join(',', $aParentIDs);
738 $aClassPlaceIDs = array();
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 .= $this->oContext->excludeSQL(' AND place_id');
749 $sSQL .= " ORDER BY rank_search ASC ";
750 $sSQL .= " LIMIT $iLimit";
752 if (CONST_Debug) var_dump($sSQL);
754 $aClassPlaceIDs = chksql($oDB->getCol($sSQL));
757 // NEAR and IN are handled the same
758 if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NEAR) {
759 $sClassTable = $this->poiTable();
760 $sSQL = "SELECT count(*) FROM pg_tables WHERE tablename = '$sClassTable'";
761 $bCacheTable = (bool) chksql($oDB->getOne($sSQL));
763 $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)";
764 if (CONST_Debug) var_dump($sSQL);
765 $iMaxRank = (int)chksql($oDB->getOne($sSQL));
767 // For state / country level searches the normal radius search doesn't work very well
769 if ($iMaxRank < 9 && $bCacheTable) {
770 // Try and get a polygon to search in instead
771 $sSQL = 'SELECT geometry FROM placex';
772 $sSQL .= " WHERE place_id in ($sPlaceIDs)";
773 $sSQL .= " AND rank_search < $iMaxRank + 5";
774 $sSQL .= " AND ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon')";
775 $sSQL .= " ORDER BY rank_search ASC ";
777 if (CONST_Debug) var_dump($sSQL);
778 $sPlaceGeom = chksql($oDB->getOne($sSQL));
785 $sSQL = 'SELECT place_id FROM placex';
786 $sSQL .= " WHERE place_id in ($sPlaceIDs) and rank_search < $iMaxRank";
787 if (CONST_Debug) var_dump($sSQL);
788 $aPlaceIDs = chksql($oDB->getCol($sSQL));
789 $sPlaceIDs = join(',', $aPlaceIDs);
792 if ($sPlaceIDs || $sPlaceGeom) {
795 // More efficient - can make the range bigger
799 if ($this->oContext->hasNearPoint()) {
800 $sOrderBySQL = $this->oContext->distanceSQL('l.centroid');
801 } elseif ($sPlaceIDs) {
802 $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
803 } elseif ($sPlaceGeom) {
804 $sOrderBySQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
807 $sSQL = 'SELECT distinct i.place_id';
809 $sSQL .= ', i.order_term';
811 $sSQL .= ' from (SELECT l.place_id';
813 $sSQL .= ','.$sOrderBySQL.' as order_term';
815 $sSQL .= ' from '.$sClassTable.' as l';
818 $sSQL .= ",placex as f WHERE ";
819 $sSQL .= "f.place_id in ($sPlaceIDs) ";
820 $sSQL .= " AND ST_DWithin(l.centroid, f.centroid, $fRange)";
821 } elseif ($sPlaceGeom) {
822 $sSQL .= " WHERE ST_Contains('$sPlaceGeom', l.centroid)";
825 $sSQL .= $this->oContext->excludeSQL(' AND l.place_id');
826 $sSQL .= 'limit 300) i ';
828 $sSQL .= 'order by order_term asc';
830 $sSQL .= " limit $iLimit";
832 if (CONST_Debug) var_dump($sSQL);
834 $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($oDB->getCol($sSQL)));
836 if ($this->oContext->hasNearPoint()) {
837 $fRange = $this->oContext->nearRadius();
841 if ($this->oContext->hasNearPoint()) {
842 $sOrderBySQL = $this->oContext->distanceSQL('l.geometry');
844 $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
847 $sSQL = 'SELECT distinct l.place_id';
849 $sSQL .= ','.$sOrderBySQL.' as orderterm';
851 $sSQL .= ' FROM placex as l, placex as f';
852 $sSQL .= " WHERE f.place_id in ($sPlaceIDs)";
853 $sSQL .= " AND ST_DWithin(l.geometry, f.centroid, $fRange)";
854 $sSQL .= " AND l.class='".$this->sClass."'";
855 $sSQL .= " AND l.type='".$this->sType."'";
856 $sSQL .= $this->oContext->excludeSQL(' AND l.place_id');
858 $sSQL .= "ORDER BY orderterm ASC";
860 $sSQL .= " limit $iLimit";
862 if (CONST_Debug) var_dump($sSQL);
864 $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($oDB->getCol($sSQL)));
869 return $aClassPlaceIDs;
873 /////////// Sort functions
876 public static function bySearchRank($a, $b)
878 if ($a->iSearchRank == $b->iSearchRank) {
879 return $a->iOperator + strlen($a->sHouseNumber)
880 - $b->iOperator - strlen($b->sHouseNumber);
883 return $a->iSearchRank < $b->iSearchRank ? -1 : 1;
886 //////////// Debugging functions
889 public function dumpAsHtmlTableRow(&$aWordIDs)
891 $kf = function ($k) use (&$aWordIDs) {
892 return $aWordIDs[$k];
896 echo "<td>$this->iSearchRank</td>";
897 echo "<td>".join(', ', array_map($kf, $this->aName))."</td>";
898 echo "<td>".join(', ', array_map($kf, $this->aNameNonSearch))."</td>";
899 echo "<td>".join(', ', array_map($kf, $this->aAddress))."</td>";
900 echo "<td>".join(', ', array_map($kf, $this->aAddressNonSearch))."</td>";
901 echo "<td>".$this->sCountryCode."</td>";
902 echo "<td>".Operator::toString($this->iOperator)."</td>";
903 echo "<td>".$this->sClass."</td>";
904 echo "<td>".$this->sType."</td>";
905 echo "<td>".$this->sPostcode."</td>";
906 echo "<td>".$this->sHouseNumber."</td>";