]> git.openstreetmap.org Git - nominatim.git/blob - lib/Geocode.php
change tiger housenumber format to save as lines instead of points and thus new inter...
[nominatim.git] / lib / Geocode.php
1 <?php
2         require_once(CONST_BasePath.'/lib/PlaceLookup.php');
3
4         class Geocode
5         {
6                 protected $oDB;
7
8                 protected $aLangPrefOrder = array();
9
10                 protected $bIncludeAddressDetails = false;
11                 protected $bIncludeExtraTags = false;
12                 protected $bIncludeNameDetails = false;
13
14                 protected $bIncludePolygonAsPoints = false;
15                 protected $bIncludePolygonAsText = false;
16                 protected $bIncludePolygonAsGeoJSON = false;
17                 protected $bIncludePolygonAsKML = false;
18                 protected $bIncludePolygonAsSVG = false;
19                 protected $fPolygonSimplificationThreshold = 0.0;
20
21                 protected $aExcludePlaceIDs = array();
22                 protected $bDeDupe = true;
23                 protected $bReverseInPlan = false;
24
25                 protected $iLimit = 20;
26                 protected $iFinalLimit = 10;
27                 protected $iOffset = 0;
28                 protected $bFallback = false;
29
30                 protected $aCountryCodes = false;
31                 protected $aNearPoint = false;
32
33                 protected $bBoundedSearch = false;
34                 protected $aViewBox = false;
35                 protected $sViewboxSmallSQL = false;
36                 protected $sViewboxLargeSQL = false;
37                 protected $aRoutePoints = false;
38
39                 protected $iMaxRank = 20;
40                 protected $iMinAddressRank = 0;
41                 protected $iMaxAddressRank = 30;
42                 protected $aAddressRankList = array();
43                 protected $exactMatchCache = array();
44
45                 protected $sAllowedTypesSQLList = false;
46
47                 protected $sQuery = false;
48                 protected $aStructuredQuery = false;
49         
50         //for Tiger housenumber interpolation
51         protected $searchedHousenumber=-1;
52         protected $housenumberFound=false;
53
54                 function Geocode(&$oDB)
55                 {
56                         $this->oDB =& $oDB;
57                 }
58
59                 function setReverseInPlan($bReverse)
60                 {
61                         $this->bReverseInPlan = $bReverse;
62                 }
63
64                 function setLanguagePreference($aLangPref)
65                 {
66                         $this->aLangPrefOrder = $aLangPref;
67                 }
68
69                 function setIncludeAddressDetails($bAddressDetails = true)
70                 {
71                         $this->bIncludeAddressDetails = (bool)$bAddressDetails;
72                 }
73
74                 function getIncludeAddressDetails()
75                 {
76                         return $this->bIncludeAddressDetails;
77                 }
78
79                 function getIncludeExtraTags()
80                 {
81                         return $this->bIncludeExtraTags;
82                 }
83
84                 function getIncludeNameDetails()
85                 {
86                         return $this->bIncludeNameDetails;
87                 }
88
89                 function setIncludePolygonAsPoints($b = true)
90                 {
91                         $this->bIncludePolygonAsPoints = $b;
92                 }
93
94                 function getIncludePolygonAsPoints()
95                 {
96                         return $this->bIncludePolygonAsPoints;
97                 }
98
99                 function setIncludePolygonAsText($b = true)
100                 {
101                         $this->bIncludePolygonAsText = $b;
102                 }
103
104                 function getIncludePolygonAsText()
105                 {
106                         return $this->bIncludePolygonAsText;
107                 }
108
109                 function setIncludePolygonAsGeoJSON($b = true)
110                 {
111                         $this->bIncludePolygonAsGeoJSON = $b;
112                 }
113
114                 function setIncludePolygonAsKML($b = true)
115                 {
116                         $this->bIncludePolygonAsKML = $b;
117                 }
118
119                 function setIncludePolygonAsSVG($b = true)
120                 {
121                         $this->bIncludePolygonAsSVG = $b;
122                 }
123
124                 function setPolygonSimplificationThreshold($f)
125                 {
126                         $this->fPolygonSimplificationThreshold = $f;
127                 }
128
129                 function setDeDupe($bDeDupe = true)
130                 {
131                         $this->bDeDupe = (bool)$bDeDupe;
132                 }
133
134                 function setLimit($iLimit = 10)
135                 {
136                         if ($iLimit > 50) $iLimit = 50;
137                         if ($iLimit < 1) $iLimit = 1;
138
139                         $this->iFinalLimit = $iLimit;
140                         $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
141                 }
142
143                 function setOffset($iOffset = 0)
144                 {
145                         $this->iOffset = $iOffset;
146                 }
147
148                 function setFallback($bFallback = true)
149                 {
150                         $this->bFallback = (bool)$bFallback;
151                 }
152
153                 function setExcludedPlaceIDs($a)
154                 {
155                         // TODO: force to int
156                         $this->aExcludePlaceIDs = $a;
157                 }
158
159                 function getExcludedPlaceIDs()
160                 {
161                         return $this->aExcludePlaceIDs;
162                 }
163
164                 function setBounded($bBoundedSearch = true)
165                 {
166                         $this->bBoundedSearch = (bool)$bBoundedSearch;
167                 }
168
169                 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
170                 {
171                         $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
172                 }
173
174                 function getViewBoxString()
175                 {
176                         if (!$this->aViewBox) return null;
177                         return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
178                 }
179
180                 function setRoute($aRoutePoints)
181                 {
182                         $this->aRoutePoints = $aRoutePoints;
183                 }
184
185                 function setFeatureType($sFeatureType)
186                 {
187                         switch($sFeatureType)
188                         {
189                         case 'country':
190                                 $this->setRankRange(4, 4);
191                                 break;
192                         case 'state':
193                                 $this->setRankRange(8, 8);
194                                 break;
195                         case 'city':
196                                 $this->setRankRange(14, 16);
197                                 break;
198                         case 'settlement':
199                                 $this->setRankRange(8, 20);
200                                 break;
201                         }
202                 }
203
204                 function setRankRange($iMin, $iMax)
205                 {
206                         $this->iMinAddressRank = (int)$iMin;
207                         $this->iMaxAddressRank = (int)$iMax;
208                 }
209
210                 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
211                 {
212                         $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
213                 }
214
215                 function setCountryCodesList($aCountryCodes)
216                 {
217                         $this->aCountryCodes = $aCountryCodes;
218                 }
219
220                 function setQuery($sQueryString)
221                 {
222                         $this->sQuery = $sQueryString;
223                         $this->aStructuredQuery = false;
224                 }
225
226                 function getQueryString()
227                 {
228                         return $this->sQuery;
229                 }
230
231
232                 function loadParamArray($aParams)
233                 {
234                         if (isset($aParams['addressdetails'])) $this->bIncludeAddressDetails = (bool)$aParams['addressdetails'];
235                         if ((float) CONST_Postgresql_Version > 9.2)
236                         {
237                                 if (isset($aParams['extratags'])) $this->bIncludeExtraTags = (bool)$aParams['extratags'];
238                                 if (isset($aParams['namedetails'])) $this->bIncludeNameDetails = (bool)$aParams['namedetails'];
239                         }
240                         if (isset($aParams['bounded'])) $this->bBoundedSearch = (bool)$aParams['bounded'];
241                         if (isset($aParams['dedupe'])) $this->bDeDupe = (bool)$aParams['dedupe'];
242
243                         if (isset($aParams['limit'])) $this->setLimit((int)$aParams['limit']);
244                         if (isset($aParams['offset'])) $this->iOffset = (int)$aParams['offset'];
245
246                         if (isset($aParams['fallback'])) $this->bFallback = (bool)$aParams['fallback'];
247
248                         // List of excluded Place IDs - used for more acurate pageing
249                         if (isset($aParams['exclude_place_ids']) && $aParams['exclude_place_ids'])
250                         {
251                                 foreach(explode(',',$aParams['exclude_place_ids']) as $iExcludedPlaceID)
252                                 {
253                                         $iExcludedPlaceID = (int)$iExcludedPlaceID;
254                                         if ($iExcludedPlaceID)
255                                                 $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID;
256                                 }
257
258                                 if (isset($aExcludePlaceIDs))
259                                         $this->aExcludePlaceIDs = $aExcludePlaceIDs;
260                         }
261
262                         // Only certain ranks of feature
263                         if (isset($aParams['featureType'])) $this->setFeatureType($aParams['featureType']);
264                         if (isset($aParams['featuretype'])) $this->setFeatureType($aParams['featuretype']);
265
266                         // Country code list
267                         if (isset($aParams['countrycodes']))
268                         {
269                                 $aCountryCodes = array();
270                                 foreach(explode(',',$aParams['countrycodes']) as $sCountryCode)
271                                 {
272                                         if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode))
273                                         {
274                                                 $aCountryCodes[] = strtolower($sCountryCode);
275                                         }
276                                 }
277                                 $this->aCountryCodes = $aCountryCodes;
278                         }
279
280                         if (isset($aParams['viewboxlbrt']) && $aParams['viewboxlbrt'])
281                         {
282                                 $aCoOrdinatesLBRT = explode(',',$aParams['viewboxlbrt']);
283                                 $this->setViewBox($aCoOrdinatesLBRT[0], $aCoOrdinatesLBRT[1], $aCoOrdinatesLBRT[2], $aCoOrdinatesLBRT[3]);
284                         }
285                         else if (isset($aParams['viewbox']) && $aParams['viewbox'])
286                         {
287                                 $aCoOrdinatesLTRB = explode(',',$aParams['viewbox']);
288                                 $this->setViewBox($aCoOrdinatesLTRB[0], $aCoOrdinatesLTRB[3], $aCoOrdinatesLTRB[2], $aCoOrdinatesLTRB[1]);
289                         }
290
291                         if (isset($aParams['route']) && $aParams['route'] && isset($aParams['routewidth']) && $aParams['routewidth'])
292                         {
293                                 $aPoints = explode(',',$aParams['route']);
294                                 if (sizeof($aPoints) % 2 != 0)
295                                 {
296                                         userError("Uneven number of points");
297                                         exit;
298                                 }
299                                 $fPrevCoord = false;
300                                 $aRoute = array();
301                                 foreach($aPoints as $i => $fPoint)
302                                 {
303                                         if ($i%2)
304                                         {
305                                                 $aRoute[] = array((float)$fPoint, $fPrevCoord);
306                                         }
307                                         else
308                                         {
309                                                 $fPrevCoord = (float)$fPoint;
310                                         }
311                                 }
312                                 $this->aRoutePoints = $aRoute;
313                         }
314                 }
315
316                 function setQueryFromParams($aParams)
317                 {
318                         // Search query
319                         $sQuery = (isset($aParams['q'])?trim($aParams['q']):'');
320                         if (!$sQuery)
321                         {
322                                 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
323                                 $this->setReverseInPlan(false);
324                         }
325                         else
326                         {
327                                 $this->setQuery($sQuery);
328                         }
329                 }
330
331                 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
332                 {
333                         $sValue = trim($sValue);
334                         if (!$sValue) return false;
335                         $this->aStructuredQuery[$sKey] = $sValue;
336                         if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30)
337                         {
338                                 $this->iMinAddressRank = $iNewMinAddressRank;
339                                 $this->iMaxAddressRank = $iNewMaxAddressRank;
340                         }
341                         if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
342                         return true;
343                 }
344
345                 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
346                 {
347                         $this->sQuery = false;
348
349                         // Reset
350                         $this->iMinAddressRank = 0;
351                         $this->iMaxAddressRank = 30;
352                         $this->aAddressRankList = array();
353
354                         $this->aStructuredQuery = array();
355                         $this->sAllowedTypesSQLList = '';
356
357                         $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
358                         $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
359                         $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
360                         $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
361                         $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
362                         $this->loadStructuredAddressElement($sPostalCode, 'postalcode' , 5, 11, array(5, 11));
363                         $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
364
365                         if (sizeof($this->aStructuredQuery) > 0) 
366                         {
367                                 $this->sQuery = join(', ', $this->aStructuredQuery);
368                                 if ($this->iMaxAddressRank < 30)
369                                 {
370                                         $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
371                                 }
372                         }
373                 }
374
375                 function fallbackStructuredQuery()
376                 {
377                         if (!$this->aStructuredQuery) return false;
378
379                         $aParams = $this->aStructuredQuery;
380
381                         if (sizeof($aParams) == 1) return false;
382
383                         $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
384
385                         foreach($aOrderToFallback as $sType)
386                         {
387                                 if (isset($aParams[$sType]))
388                                 {
389                                         unset($aParams[$sType]);
390                                         $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
391                                         return true;
392                                 }
393                         }
394
395                         return false;
396                 }
397
398                 function getDetails($aPlaceIDs)
399                 {
400                         if (sizeof($aPlaceIDs) == 0)  return array();
401
402                         $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
403
404                         // Get the details for display (is this a redundant extra step?)
405                         $sPlaceIDs = join(',',$aPlaceIDs);
406
407                         $sImportanceSQL = '';
408                         if ($this->sViewboxSmallSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
409                         if ($this->sViewboxLargeSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
410
411                         $sSQL = "select osm_type,osm_id,class,type,admin_level,rank_search,rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id, calculated_country_code as country_code,";
412                         $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
413                         $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
414                         $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
415                         if ($this->bIncludeExtraTags) $sSQL .= "hstore_to_json(extratags)::text as extra,";
416                         if ($this->bIncludeNameDetails) $sSQL .= "hstore_to_json(name)::text as names,";
417                         $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
418                         $sSQL .= $sImportanceSQL."coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
419                         $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(CASE WHEN placex.rank_search < 28 THEN placex.place_id ELSE placex.parent_place_id END) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
420                         $sSQL .= "(extratags->'place') as extra_place ";
421                         $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
422                         $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
423                         if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
424                         if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
425                         $sSQL .= ") ";
426                         if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
427                         $sSQL .= "and linked_place_id is null ";
428                         $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
429                         if (!$this->bDeDupe) $sSQL .= ",place_id";
430                         $sSQL .= ",langaddress ";
431                         $sSQL .= ",placename ";
432                         $sSQL .= ",ref ";
433                         if ($this->bIncludeExtraTags) $sSQL .= ",extratags";
434                         if ($this->bIncludeNameDetails) $sSQL .= ",name";
435                         $sSQL .= ",extratags->'place' ";
436
437                         if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
438                         {
439                                 //query also location_property_tiger_line and location_property_aux
440                 //Tiger search only if it was searched for a housenumber (searchedHousenumber >=0) and if it was found (housenumberFound = true)
441                 //only Tiger housenumbers need to be interpolated, because they are saved as lines with start- and endnumber, the common osm housenumbers are usually saved as points
442                 if($this->searchedHousenumber>=0 && $this->housenumberFound){
443                     $sSQL .= "union ";
444                     $sSQL .= "select 'T' as osm_type, place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id,'us' as country_code";
445                     $sSQL .= ", get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress ";
446                     $sSQL .= ", null as placename";
447                     $sSQL .= ", null as ref";
448                     if ($this->bIncludeExtraTags) $sSQL .= ", null as extra";
449                     if ($this->bIncludeNameDetails) $sSQL .= ", null as names";
450                     $sSQL .= ", avg(st_x(point)) as lon, avg(st_y(point)) as lat";
451                     $sSQL .= $sImportanceSQL.", -1.15 as importance ";
452                     $sSQL .= ", 1.0 as addressimportance "; //not sure how the addressimportance is/should be calculated for Tiger data
453                     $sSQL .= ", null as extra_place ";
454                     $sSQL .= " from (select place_id";
455                     //interpolate the Tiger housenumbers here
456                     $sSQL .= ",ST_LineInterpolatePoint(linegeo, ($this->searchedHousenumber::float-startnumber::float)/(endnumber-startnumber)::float) as point, parent_place_id ";
457                     $sSQL .= "from location_property_tiger_line where place_id in ($sPlaceIDs) ";
458                     $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank) as blub"; //postgres wants an alias here
459                     $sSQL .= " group by place_id"; //why group by place_id, isnt place_id unique?
460                     if (!$this->bDeDupe) $sSQL .= ",place_id ";
461                 }
462                                 $sSQL .= " union ";
463                                 $sSQL .= "select 'L' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id,'us' as country_code,";
464                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
465                                 $sSQL .= "null as placename,";
466                                 $sSQL .= "null as ref,";
467                                 if ($this->bIncludeExtraTags) $sSQL .= "null as extra,";
468                                 if ($this->bIncludeNameDetails) $sSQL .= "null as names,";
469                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
470                                 $sSQL .= $sImportanceSQL."-1.10 as importance, ";
471                                 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_aux.parent_place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
472                                 $sSQL .= "null as extra_place ";
473                                 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
474                                 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
475                                 $sSQL .= "group by place_id";
476                                 if (!$this->bDeDupe) $sSQL .= ",place_id";
477                                 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
478                         }
479
480                         $sSQL .= " order by importance desc";
481                         if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
482                         $aSearchResults = $this->oDB->getAll($sSQL);
483
484                         if (PEAR::IsError($aSearchResults))
485                         {
486                                 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
487                         }
488
489                         return $aSearchResults;
490                 }
491
492                 function getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases)
493                 {
494                         /*
495                                  Calculate all searches using aValidTokens i.e.
496                                  'Wodsworth Road, Sheffield' =>
497
498                                  Phrase Wordset
499                                  0      0       (wodsworth road)
500                                  0      1       (wodsworth)(road)
501                                  1      0       (sheffield)
502
503                                  Score how good the search is so they can be ordered
504                          */
505                         foreach($aPhrases as $iPhrase => $sPhrase)
506                         {
507                                 $aNewPhraseSearches = array();
508                                 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
509                                 else $sPhraseType = '';
510
511                                 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
512                                 {
513                                         // Too many permutations - too expensive
514                                         if ($iWordSet > 120) break;
515
516                                         $aWordsetSearches = $aSearches;
517
518                                         // Add all words from this wordset
519                                         foreach($aWordset as $iToken => $sToken)
520                                         {
521                                                 //echo "<br><b>$sToken</b>";
522                                                 $aNewWordsetSearches = array();
523
524                                                 foreach($aWordsetSearches as $aCurrentSearch)
525                                                 {
526                                                         //echo "<i>";
527                                                         //var_dump($aCurrentSearch);
528                                                         //echo "</i>";
529
530                                                         // If the token is valid
531                                                         if (isset($aValidTokens[' '.$sToken]))
532                                                         {
533                                                                 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
534                                                                 {
535                                                                         $aSearch = $aCurrentSearch;
536                                                                         $aSearch['iSearchRank']++;
537                                                                         if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
538                                                                         {
539                                                                                 if ($aSearch['sCountryCode'] === false)
540                                                                                 {
541                                                                                         $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
542                                                                                         // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
543                                                                                         if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)))
544                                                                                         {
545                                                                                                 $aSearch['iSearchRank'] += 5;
546                                                                                         }
547                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
548                                                                                 }
549                                                                         }
550                                                                         elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
551                                                                         {
552                                                                                 if ($aSearch['fLat'] === '')
553                                                                                 {
554                                                                                         $aSearch['fLat'] = $aSearchTerm['lat'];
555                                                                                         $aSearch['fLon'] = $aSearchTerm['lon'];
556                                                                                         $aSearch['fRadius'] = $aSearchTerm['radius'];
557                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
558                                                                                 }
559                                                                         }
560                                                                         elseif ($sPhraseType == 'postalcode')
561                                                                         {
562                                                                                 // We need to try the case where the postal code is the primary element (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode) so try both
563                                                                                 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
564                                                                                 {
565                                                                                         // If we already have a name try putting the postcode first
566                                                                                         if (sizeof($aSearch['aName']))
567                                                                                         {
568                                                                                                 $aNewSearch = $aSearch;
569                                                                                                 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
570                                                                                                 $aNewSearch['aName'] = array();
571                                                                                                 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
572                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
573                                                                                         }
574
575                                                                                         if (sizeof($aSearch['aName']))
576                                                                                         {
577                                                                                                 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false))
578                                                                                                 {
579                                                                                                         $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
580                                                                                                 }
581                                                                                                 else
582                                                                                                 {
583                                                                                                         $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
584                                                                                                         $aSearch['iSearchRank'] += 1000; // skip;
585                                                                                                 }
586                                                                                         }
587                                                                                         else
588                                                                                         {
589                                                                                                 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
590                                                                                                 //$aSearch['iNamePhrase'] = $iPhrase;
591                                                                                         }
592                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
593                                                                                 }
594
595                                                                         }
596                                                                         elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
597                                                                         {
598                                                                                 if ($aSearch['sHouseNumber'] === '')
599                                                                                 {
600                                                                                         $aSearch['sHouseNumber'] = $sToken;
601                                                                                         // sanity check: if the housenumber is not mainly made
602                                                                                         // up of numbers, add a penalty
603                                                                                         if (preg_match_all("/[^0-9]/", $sToken, $aMatches) > 2) $aSearch['iSearchRank']++;
604                                                                                         // also housenumbers should appear in the first or second phrase
605                                                                                         if ($iPhrase > 1) $aSearch['iSearchRank'] += 1;
606                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
607                                                                                         /*
608                                                                                         // Fall back to not searching for this item (better than nothing)
609                                                                                         $aSearch = $aCurrentSearch;
610                                                                                         $aSearch['iSearchRank'] += 1;
611                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
612                                                                                          */
613                                                                                 }
614                                                                         }
615                                                                         elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
616                                                                         {
617                                                                                 if ($aSearch['sClass'] === '')
618                                                                                 {
619                                                                                         $aSearch['sOperator'] = $aSearchTerm['operator'];
620                                                                                         $aSearch['sClass'] = $aSearchTerm['class'];
621                                                                                         $aSearch['sType'] = $aSearchTerm['type'];
622                                                                                         if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
623                                                                                         else $aSearch['sOperator'] = 'near'; // near = in for the moment
624                                                                                         if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
625
626                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
627                                                                                 }
628                                                                         }
629                                                                         elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
630                                                                         {
631                                                                                 if (sizeof($aSearch['aName']))
632                                                                                 {
633                                                                                         if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false))
634                                                                                         {
635                                                                                                 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
636                                                                                         }
637                                                                                         else
638                                                                                         {
639                                                                                                 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
640                                                                                                 $aSearch['iSearchRank'] += 1000; // skip;
641                                                                                         }
642                                                                                 }
643                                                                                 else
644                                                                                 {
645                                                                                         $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
646                                                                                         //$aSearch['iNamePhrase'] = $iPhrase;
647                                                                                 }
648                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
649                                                                         }
650                                                                 }
651                                                         }
652                                                         // Look for partial matches.
653                                                         // Note that there is no point in adding country terms here
654                                                         // because country are omitted in the address.
655                                                         if (isset($aValidTokens[$sToken]) && $sPhraseType != 'country')
656                                                         {
657                                                                 // Allow searching for a word - but at extra cost
658                                                                 foreach($aValidTokens[$sToken] as $aSearchTerm)
659                                                                 {
660                                                                         if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
661                                                                         {
662                                                                                 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strpos($sToken, ' ') === false)
663                                                                                 {
664                                                                                         $aSearch = $aCurrentSearch;
665                                                                                         $aSearch['iSearchRank'] += 1;
666                                                                                         if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
667                                                                                         {
668                                                                                                 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
669                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
670                                                                                         }
671                                                                                         elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
672                                                                                         {
673                                                                                                 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
674                                                                                                 $aSearch['iSearchRank'] += 1;
675                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
676                                                                                                 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
677                                                                                                 {
678                                                                                                         if (empty($aSearchTermToken['country_code'])
679                                                                                                                         && empty($aSearchTermToken['lat'])
680                                                                                                                         && empty($aSearchTermToken['class']))
681                                                                                                         {
682                                                                                                                 $aSearch = $aCurrentSearch;
683                                                                                                                 $aSearch['iSearchRank'] += 1;
684                                                                                                                 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
685                                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
686                                                                                                         }
687                                                                                                 }
688                                                                                         }
689                                                                                         else
690                                                                                         {
691                                                                                                 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
692                                                                                                 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
693                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
694                                                                                         }
695                                                                                 }
696
697                                                                                 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
698                                                                                 {
699                                                                                         $aSearch = $aCurrentSearch;
700                                                                                         $aSearch['iSearchRank'] += 1;
701                                                                                         if (!sizeof($aCurrentSearch['aName'])) $aSearch['iSearchRank'] += 1;
702                                                                                         if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
703                                                                                         if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
704                                                                                                 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
705                                                                                         else
706                                                                                                 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
707                                                                                         $aSearch['iNamePhrase'] = $iPhrase;
708                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
709                                                                                 }
710                                                                         }
711                                                                 }
712                                                         }
713                                                         else
714                                                         {
715                                                                 // Allow skipping a word - but at EXTREAM cost
716                                                                 //$aSearch = $aCurrentSearch;
717                                                                 //$aSearch['iSearchRank']+=100;
718                                                                 //$aNewWordsetSearches[] = $aSearch;
719                                                         }
720                                                 }
721                                                 // Sort and cut
722                                                 usort($aNewWordsetSearches, 'bySearchRank');
723                                                 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
724                                         }
725                                         //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
726
727                                         $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
728                                         usort($aNewPhraseSearches, 'bySearchRank');
729
730                                         $aSearchHash = array();
731                                         foreach($aNewPhraseSearches as $iSearch => $aSearch)
732                                         {
733                                                 $sHash = serialize($aSearch);
734                                                 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
735                                                 else $aSearchHash[$sHash] = 1;
736                                         }
737
738                                         $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
739                                 }
740
741                                 // Re-group the searches by their score, junk anything over 20 as just not worth trying
742                                 $aGroupedSearches = array();
743                                 foreach($aNewPhraseSearches as $aSearch)
744                                 {
745                                         if ($aSearch['iSearchRank'] < $this->iMaxRank)
746                                         {
747                                                 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
748                                                 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
749                                         }
750                                 }
751                                 ksort($aGroupedSearches);
752
753                                 $iSearchCount = 0;
754                                 $aSearches = array();
755                                 foreach($aGroupedSearches as $iScore => $aNewSearches)
756                                 {
757                                         $iSearchCount += sizeof($aNewSearches);
758                                         $aSearches = array_merge($aSearches, $aNewSearches);
759                                         if ($iSearchCount > 50) break;
760                                 }
761
762                                 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
763
764                         }
765                         return $aGroupedSearches;
766
767                 }
768
769                 /* Perform the actual query lookup.
770
771                         Returns an ordered list of results, each with the following fields:
772                                 osm_type: type of corresponding OSM object
773                                                         N - node
774                                                         W - way
775                                                         R - relation
776                                                         P - postcode (internally computed)
777                                 osm_id: id of corresponding OSM object
778                                 class: general object class (corresponds to tag key of primary OSM tag)
779                                 type: subclass of object (corresponds to tag value of primary OSM tag)
780                                 admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
781                                 rank_search: rank in search hierarchy
782                                                         (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
783                                 rank_address: rank in address hierarchy (determines orer in address)
784                                 place_id: internal key (may differ between different instances)
785                                 country_code: ISO country code
786                                 langaddress: localized full address
787                                 placename: localized name of object
788                                 ref: content of ref tag (if available)
789                                 lon: longitude
790                                 lat: latitude
791                                 importance: importance of place based on Wikipedia link count
792                                 addressimportance: cumulated importance of address elements
793                                 extra_place: type of place (for admin boundaries, if there is a place tag)
794                                 aBoundingBox: bounding Box
795                                 label: short description of the object class/type (English only) 
796                                 name: full name (currently the same as langaddress)
797                                 foundorder: secondary ordering for places with same importance
798                 */
799                 function lookup()
800                 {
801                         if (!$this->sQuery && !$this->aStructuredQuery) return false;
802
803                         $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
804                         $sCountryCodesSQL = false;
805                         if ($this->aCountryCodes && sizeof($this->aCountryCodes))
806                         {
807                                 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
808                         }
809
810                         $sQuery = $this->sQuery;
811
812                         // Conflicts between US state abreviations and various words for 'the' in different languages
813                         if (isset($this->aLangPrefOrder['name:en']))
814                         {
815                                 $sQuery = preg_replace('/(^|,)\s*il\s*(,|$)/','\1illinois\2', $sQuery);
816                                 $sQuery = preg_replace('/(^|,)\s*al\s*(,|$)/','\1alabama\2', $sQuery);
817                                 $sQuery = preg_replace('/(^|,)\s*la\s*(,|$)/','\1louisiana\2', $sQuery);
818                         }
819
820                         // View Box SQL
821                         $sViewboxCentreSQL = false;
822                         $bBoundingBoxSearch = false;
823                         if ($this->aViewBox)
824                         {
825                                 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
826                                 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
827                                 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
828                                 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
829                                 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
830                                 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
831
832                                 $this->sViewboxSmallSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$this->aViewBox[0].",".(float)$this->aViewBox[1]."),ST_Point(".(float)$this->aViewBox[2].",".(float)$this->aViewBox[3].")),4326)";
833                                 $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
834                                 $bBoundingBoxSearch = $this->bBoundedSearch;
835                         }
836
837                         // Route SQL
838                         if ($this->aRoutePoints)
839                         {
840                                 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
841                                 $bFirst = true;
842                                 foreach($this->aRoutePoints as $aPoint)
843                                 {
844                                         if (!$bFirst) $sViewboxCentreSQL .= ",";
845                                         $sViewboxCentreSQL .= $aPoint[0].' '.$aPoint[1];
846                                         $bFirst = false;
847                                 }
848                                 $sViewboxCentreSQL .= ")'::geometry,4326)";
849
850                                 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
851                                 $this->sViewboxSmallSQL = $this->oDB->getOne($sSQL);
852                                 if (PEAR::isError($this->sViewboxSmallSQL))
853                                 {
854                                         failInternalError("Could not get small viewbox.", $sSQL, $this->sViewboxSmallSQL);
855                                 }
856                                 $this->sViewboxSmallSQL = "'".$this->sViewboxSmallSQL."'::geometry";
857
858                                 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
859                                 $this->sViewboxLargeSQL = $this->oDB->getOne($sSQL);
860                                 if (PEAR::isError($this->sViewboxLargeSQL))
861                                 {
862                                         failInternalError("Could not get large viewbox.", $sSQL, $this->sViewboxLargeSQL);
863                                 }
864                                 $this->sViewboxLargeSQL = "'".$this->sViewboxLargeSQL."'::geometry";
865                                 $bBoundingBoxSearch = $this->bBoundedSearch;
866                         }
867
868                         // Do we have anything that looks like a lat/lon pair?
869                         if ( $aLooksLike = looksLikeLatLonPair($sQuery) ){
870                                 $this->setNearPoint(array($aLooksLike['lat'], $aLooksLike['lon']));
871                                 $sQuery = $aLooksLike['query'];                 
872                         }
873
874                         $aSearchResults = array();
875                         if ($sQuery || $this->aStructuredQuery)
876                         {
877                                 // Start with a blank search
878                                 $aSearches = array(
879                                         array('iSearchRank' => 0, 
880                                                                 'iNamePhrase' => -1, 
881                                                                 'sCountryCode' => false, 
882                                                                 'aName' => array(), 
883                                                                 'aAddress' => array(), 
884                                                                 'aFullNameAddress' => array(),
885                                                                 'aNameNonSearch' => array(), 
886                                                                 'aAddressNonSearch' => array(),
887                                                                 'sOperator' => '', 
888                                                                 'aFeatureName' => array(), 
889                                                                 'sClass' => '', 
890                                                                 'sType' => '', 
891                                                                 'sHouseNumber' => '', 
892                                                                 'fLat' => '', 
893                                                                 'fLon' => '', 
894                                                                 'fRadius' => ''
895                                                         )
896                                 );
897
898                                 // Do we have a radius search?
899                                 $sNearPointSQL = false;
900                                 if ($this->aNearPoint)
901                                 {
902                                         $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
903                                         $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
904                                         $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
905                                         $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
906                                 }
907
908                                 // Any 'special' terms in the search?
909                                 $bSpecialTerms = false;
910                                 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
911                                 $aSpecialTerms = array();
912                                 foreach($aSpecialTermsRaw as $aSpecialTerm)
913                                 {
914                                         $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
915                                         $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
916                                 }
917
918                                 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
919                                 $aSpecialTerms = array();
920                                 if (isset($this->aStructuredQuery['amenity']) && $this->aStructuredQuery['amenity'])
921                                 {
922                                         $aSpecialTermsRaw[] = array('['.$this->aStructuredQuery['amenity'].']', $this->aStructuredQuery['amenity']);
923                                         unset($this->aStructuredQuery['amenity']);
924                                 }
925                                 foreach($aSpecialTermsRaw as $aSpecialTerm)
926                                 {
927                                         $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
928                                         $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
929                                         $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
930                                         $sSQL .= ' from word where word_token in (\' '.$sToken.'\')) as x where (class is not null and class not in (\'place\')) or country_code is not null';
931                                         if (CONST_Debug) var_Dump($sSQL);
932                                         $aSearchWords = $this->oDB->getAll($sSQL);
933                                         $aNewSearches = array();
934                                         foreach($aSearches as $aSearch)
935                                         {
936                                                 foreach($aSearchWords as $aSearchTerm)
937                                                 {
938                                                         $aNewSearch = $aSearch;
939                                                         if ($aSearchTerm['country_code'])
940                                                         {
941                                                                 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
942                                                                 $aNewSearches[] = $aNewSearch;
943                                                                 $bSpecialTerms = true;
944                                                         }
945                                                         if ($aSearchTerm['class'])
946                                                         {
947                                                                 $aNewSearch['sClass'] = $aSearchTerm['class'];
948                                                                 $aNewSearch['sType'] = $aSearchTerm['type'];
949                                                                 $aNewSearches[] = $aNewSearch;
950                                                                 $bSpecialTerms = true;
951                                                         }
952                                                 }
953                                         }
954                                         $aSearches = $aNewSearches;
955                                 }
956
957                                 // Split query into phrases
958                                 // Commas are used to reduce the search space by indicating where phrases split
959                                 if ($this->aStructuredQuery)
960                                 {
961                                         $aPhrases = $this->aStructuredQuery;
962                                         $bStructuredPhrases = true;
963                                 }
964                                 else
965                                 {
966                                         $aPhrases = explode(',',$sQuery);
967                                         $bStructuredPhrases = false;
968                                 }
969
970                                 // Convert each phrase to standard form
971                                 // Create a list of standard words
972                                 // Get all 'sets' of words
973                                 // Generate a complete list of all
974                                 $aTokens = array();
975                                 foreach($aPhrases as $iPhrase => $sPhrase)
976                                 {
977                                         $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
978                                         if (PEAR::isError($aPhrase))
979                                         {
980                                                 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
981                                                 if (CONST_Debug) var_dump($aPhrase);
982                                                 exit;
983                                         }
984                                         if (trim($aPhrase['string']))
985                                         {
986                                                 $aPhrases[$iPhrase] = $aPhrase;
987                                                 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
988                                                 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
989                                                 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
990                                         }
991                                         else
992                                         {
993                                                 unset($aPhrases[$iPhrase]);
994                                         }
995                                 }
996
997                                 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
998                                 $aPhraseTypes = array_keys($aPhrases);
999                                 $aPhrases = array_values($aPhrases);
1000
1001                                 if (sizeof($aTokens))
1002                                 {
1003                                         // Check which tokens we have, get the ID numbers
1004                                         $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
1005                                         $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
1006
1007                                         if (CONST_Debug) var_Dump($sSQL);
1008
1009                                         $aValidTokens = array();
1010                                         if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
1011                                         else $aDatabaseWords = array();
1012                                         if (PEAR::IsError($aDatabaseWords))
1013                                         {
1014                                                 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
1015                                         }
1016                                         $aPossibleMainWordIDs = array();
1017                                         $aWordFrequencyScores = array();
1018                                         foreach($aDatabaseWords as $aToken)
1019                                         {
1020                                                 // Very special case - require 2 letter country param to match the country code found
1021                                                 if ($bStructuredPhrases && $aToken['country_code'] && !empty($this->aStructuredQuery['country'])
1022                                                                 && strlen($this->aStructuredQuery['country']) == 2 && strtolower($this->aStructuredQuery['country']) != $aToken['country_code'])
1023                                                 {
1024                                                         continue;
1025                                                 }
1026
1027                                                 if (isset($aValidTokens[$aToken['word_token']]))
1028                                                 {
1029                                                         $aValidTokens[$aToken['word_token']][] = $aToken;
1030                                                 }
1031                                                 else
1032                                                 {
1033                                                         $aValidTokens[$aToken['word_token']] = array($aToken);
1034                                                 }
1035                                                 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
1036                                                 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
1037                                         }
1038                                         if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
1039
1040                                         // Try and calculate GB postcodes we might be missing
1041                                         foreach($aTokens as $sToken)
1042                                         {
1043                                                 // Source of gb postcodes is now definitive - always use
1044                                                 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
1045                                                 {
1046                                                         if (substr($aData[1],-2,1) != ' ')
1047                                                         {
1048                                                                 $aData[0] = substr($aData[0],0,strlen($aData[1])-1).' '.substr($aData[0],strlen($aData[1])-1);
1049                                                                 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
1050                                                         }
1051                                                         $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
1052                                                         if ($aGBPostcodeLocation)
1053                                                         {
1054                                                                 $aValidTokens[$sToken] = $aGBPostcodeLocation;
1055                                                         }
1056                                                 }
1057                                                 // US ZIP+4 codes - if there is no token,
1058                                                 //      merge in the 5-digit ZIP code
1059                                                 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
1060                                                 {
1061                                                         if (isset($aValidTokens[$aData[1]]))
1062                                                         {
1063                                                                 foreach($aValidTokens[$aData[1]] as $aToken)
1064                                                                 {
1065                                                                         if (!$aToken['class'])
1066                                                                         {
1067                                                                                 if (isset($aValidTokens[$sToken]))
1068                                                                                 {
1069                                                                                         $aValidTokens[$sToken][] = $aToken;
1070                                                                                 }
1071                                                                                 else
1072                                                                                 {
1073                                                                                         $aValidTokens[$sToken] = array($aToken);
1074                                                                                 }
1075                                                                         }
1076                                                                 }
1077                                                         }
1078                                                 }
1079                                         }
1080
1081                                         foreach($aTokens as $sToken)
1082                                         {
1083                                                 // Unknown single word token with a number - assume it is a house number
1084                                                 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
1085                                                 {
1086                                                         $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
1087                                                 }
1088                                         }
1089
1090                                         // Any words that have failed completely?
1091                                         // TODO: suggestions
1092
1093                                         // Start the search process
1094                                         $aResultPlaceIDs = array();
1095
1096                                         $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases);
1097
1098                                         if ($this->bReverseInPlan)
1099                                         {
1100                                                 // Reverse phrase array and also reverse the order of the wordsets in
1101                                                 // the first and final phrase. Don't bother about phrases in the middle
1102                                                 // because order in the address doesn't matter.
1103                                                 $aPhrases = array_reverse($aPhrases);
1104                                                 $aPhrases[0]['wordsets'] = getInverseWordSets($aPhrases[0]['words'], 0);
1105                                                 if (sizeof($aPhrases) > 1)
1106                                                 {
1107                                                         $aFinalPhrase = end($aPhrases);
1108                                                         $aPhrases[sizeof($aPhrases)-1]['wordsets'] = getInverseWordSets($aFinalPhrase['words'], 0);
1109                                                 }
1110                                                 $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, null, $aPhrases, $aValidTokens, $aWordFrequencyScores, false);
1111
1112                                                 foreach($aGroupedSearches as $aSearches)
1113                                                 {
1114                                                         foreach($aSearches as $aSearch)
1115                                                         {
1116                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1117                                                                 {
1118                                                                         if (!isset($aReverseGroupedSearches[$aSearch['iSearchRank']])) $aReverseGroupedSearches[$aSearch['iSearchRank']] = array();
1119                                                                         $aReverseGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1120                                                                 }
1121
1122                                                         }
1123                                                 }
1124
1125                                                 $aGroupedSearches = $aReverseGroupedSearches;
1126                                                 ksort($aGroupedSearches);
1127                                         }
1128                                 }
1129                                 else
1130                                 {
1131                                         // Re-group the searches by their score, junk anything over 20 as just not worth trying
1132                                         $aGroupedSearches = array();
1133                                         foreach($aSearches as $aSearch)
1134                                         {
1135                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1136                                                 {
1137                                                         if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
1138                                                         $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1139                                                 }
1140                                         }
1141                                         ksort($aGroupedSearches);
1142                                 }
1143
1144                                 if (CONST_Debug) var_Dump($aGroupedSearches);
1145
1146                                 if (CONST_Search_TryDroppedAddressTerms && sizeof($this->aStructuredQuery) > 0)
1147                                 {
1148                                         $aCopyGroupedSearches = $aGroupedSearches;
1149                                         foreach($aCopyGroupedSearches as $iGroup => $aSearches)
1150                                         {
1151                                                 foreach($aSearches as $iSearch => $aSearch)
1152                                                 {
1153                                                         $aReductionsList = array($aSearch['aAddress']);
1154                                                         $iSearchRank = $aSearch['iSearchRank'];
1155                                                         while(sizeof($aReductionsList) > 0)
1156                                                         {
1157                                                                 $iSearchRank += 5;
1158                                                                 if ($iSearchRank > iMaxRank) break 3;
1159                                                                 $aNewReductionsList = array();
1160                                                                 foreach($aReductionsList as $aReductionsWordList)
1161                                                                 {
1162                                                                         for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
1163                                                                         {
1164                                                                                 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
1165                                                                                 $aReverseSearch = $aSearch;
1166                                                                                 $aSearch['aAddress'] = $aReductionsWordListResult;
1167                                                                                 $aSearch['iSearchRank'] = $iSearchRank;
1168                                                                                 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
1169                                                                                 if (sizeof($aReductionsWordListResult) > 0)
1170                                                                                 {
1171                                                                                         $aNewReductionsList[] = $aReductionsWordListResult;
1172                                                                                 }
1173                                                                         }
1174                                                                 }
1175                                                                 $aReductionsList = $aNewReductionsList;
1176                                                         }
1177                                                 }
1178                                         }
1179                                         ksort($aGroupedSearches);
1180                                 }
1181
1182                                 // Filter out duplicate searches
1183                                 $aSearchHash = array();
1184                                 foreach($aGroupedSearches as $iGroup => $aSearches)
1185                                 {
1186                                         foreach($aSearches as $iSearch => $aSearch)
1187                                         {
1188                                                 $sHash = serialize($aSearch);
1189                                                 if (isset($aSearchHash[$sHash]))
1190                                                 {
1191                                                         unset($aGroupedSearches[$iGroup][$iSearch]);
1192                                                         if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1193                                                 }
1194                                                 else
1195                                                 {
1196                                                         $aSearchHash[$sHash] = 1;
1197                                                 }
1198                                         }
1199                                 }
1200
1201                                 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1202
1203                                 $iGroupLoop = 0;
1204                                 $iQueryLoop = 0;
1205                                 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1206                                 {
1207                                         $iGroupLoop++;
1208                                         foreach($aSearches as $aSearch)
1209                                         {
1210                                                 $iQueryLoop++;
1211
1212                                                 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1213                                                 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1214
1215                                                 // No location term?
1216                                                 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1217                                                 {
1218                                                         if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1219                                                         {
1220                                                                 // Just looking for a country by code - look it up
1221                                                                 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1222                                                                 {
1223                                                                         $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1224                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1225                                                                         if ($bBoundingBoxSearch)
1226                                                                                 $sSQL .= " and _st_intersects($this->sViewboxSmallSQL, geometry)";
1227                                                                         $sSQL .= " order by st_area(geometry) desc limit 1";
1228                                                                         if (CONST_Debug) var_dump($sSQL);
1229                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1230                                                                 }
1231                                                                 else
1232                                                                 {
1233                                                                         $aPlaceIDs = array();
1234                                                                 }
1235                                                         }
1236                                                         else
1237                                                         {
1238                                                                 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1239                                                                 if (!$aSearch['sClass']) continue;
1240                                                                 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1241                                                                 if ($this->oDB->getOne($sSQL))
1242                                                                 {
1243                                                                         $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1244                                                                         if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1245                                                                         $sSQL .= " where st_contains($this->sViewboxSmallSQL, ct.centroid)";
1246                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1247                                                                         if (sizeof($this->aExcludePlaceIDs))
1248                                                                         {
1249                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1250                                                                         }
1251                                                                         if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1252                                                                         $sSQL .= " limit $this->iLimit";
1253                                                                         if (CONST_Debug) var_dump($sSQL);
1254                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1255
1256                                                                         // If excluded place IDs are given, it is fair to assume that
1257                                                                         // there have been results in the small box, so no further
1258                                                                         // expansion in that case.
1259                                                                         // Also don't expand if bounded results were requested.
1260                                                                         if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs) && !$this->bBoundedSearch)
1261                                                                         {
1262                                                                                 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1263                                                                                 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1264                                                                                 $sSQL .= " where st_contains($this->sViewboxLargeSQL, ct.centroid)";
1265                                                                                 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1266                                                                                 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1267                                                                                 $sSQL .= " limit $this->iLimit";
1268                                                                                 if (CONST_Debug) var_dump($sSQL);
1269                                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1270                                                                         }
1271                                                                 }
1272                                                                 else
1273                                                                 {
1274                                                                         $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1275                                                                         $sSQL .= " and st_contains($this->sViewboxSmallSQL, geometry) and linked_place_id is null";
1276                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1277                                                                         if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1278                                                                         $sSQL .= " limit $this->iLimit";
1279                                                                         if (CONST_Debug) var_dump($sSQL);
1280                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1281                                                                 }
1282                                                         }
1283                                                 }
1284                                                 else
1285                                                 {
1286                                                         $aPlaceIDs = array();
1287
1288                                                         // First we need a position, either aName or fLat or both
1289                                                         $aTerms = array();
1290                                                         $aOrder = array();
1291
1292                                                         if ($aSearch['sHouseNumber'] && sizeof($aSearch['aAddress']))
1293                                                         {
1294                                                                 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1295                                                                 $aOrder[] = "exists(select place_id from placex where parent_place_id = search_name.place_id and transliteration(housenumber) ~* E'".$sHouseNumberRegex."' limit 1) desc";
1296                                                         }
1297
1298                                                         // TODO: filter out the pointless search terms (2 letter name tokens and less)
1299                                                         // they might be right - but they are just too darned expensive to run
1300                                                         if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1301                                                         if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1302                                                         if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1303                                                         {
1304                                                                 // For infrequent name terms disable index usage for address
1305                                                                 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1306                                                                                 sizeof($aSearch['aName']) == 1 &&
1307                                                                                 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1308                                                                 {
1309                                                                         $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1310                                                                 }
1311                                                                 else
1312                                                                 {
1313                                                                         $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1314                                                                         if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1315                                                                 }
1316                                                         }
1317                                                         if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1318                                                         if ($aSearch['sHouseNumber'])
1319                                                         {
1320                                                                 $aTerms[] = "address_rank between 16 and 27";
1321                                                         }
1322                                                         else
1323                                                         {
1324                                                                 if ($this->iMinAddressRank > 0)
1325                                                                 {
1326                                                                         $aTerms[] = "address_rank >= ".$this->iMinAddressRank;
1327                                                                 }
1328                                                                 if ($this->iMaxAddressRank < 30)
1329                                                                 {
1330                                                                         $aTerms[] = "address_rank <= ".$this->iMaxAddressRank;
1331                                                                 }
1332                                                         }
1333                                                         if ($aSearch['fLon'] && $aSearch['fLat'])
1334                                                         {
1335                                                                 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1336                                                                 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1337                                                         }
1338                                                         if (sizeof($this->aExcludePlaceIDs))
1339                                                         {
1340                                                                 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1341                                                         }
1342                                                         if ($sCountryCodesSQL)
1343                                                         {
1344                                                                 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1345                                                         }
1346
1347                                                         if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1348                                                         if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1349
1350                                                         if ($aSearch['sHouseNumber'])
1351                                                         {
1352                                                                 $sImportanceSQL = '- abs(26 - address_rank) + 3';
1353                                                         }
1354                                                         else
1355                                                         {
1356                                                                 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1357                                                         }
1358                                                         if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1359                                                         if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1360
1361                                                         $aOrder[] = "$sImportanceSQL DESC";
1362                                                         if (sizeof($aSearch['aFullNameAddress']))
1363                                                         {
1364                                                                 $sExactMatchSQL = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) as exactmatch';
1365                                                                 $aOrder[] = 'exactmatch DESC';
1366                                                         } else {
1367                                                                 $sExactMatchSQL = '0::int as exactmatch';
1368                                                         }
1369
1370                                                         if (sizeof($aTerms))
1371                                                         {
1372                                                                 $sSQL = "select place_id, ";
1373                                                                 $sSQL .= $sExactMatchSQL;
1374                                                                 $sSQL .= " from search_name";
1375                                                                 $sSQL .= " where ".join(' and ',$aTerms);
1376                                                                 $sSQL .= " order by ".join(', ',$aOrder);
1377                                                                 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1378                                                                         $sSQL .= " limit 20";
1379                                                                 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1380                                                                         $sSQL .= " limit 1";
1381                                                                 else
1382                                                                         $sSQL .= " limit ".$this->iLimit;
1383
1384                                                                 if (CONST_Debug) { var_dump($sSQL); }
1385                                                                 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1386                                                                 if (PEAR::IsError($aViewBoxPlaceIDs))
1387                                                                 {
1388                                                                         failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1389                                                                 }
1390                                                                 //var_dump($aViewBoxPlaceIDs);
1391                                                                 // Did we have an viewbox matches?
1392                                                                 $aPlaceIDs = array();
1393                                                                 $bViewBoxMatch = false;
1394                                                                 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1395                                                                 {
1396                                                                         //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1397                                                                         //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1398                                                                         //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1399                                                                         //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1400                                                                         $aPlaceIDs[] = $aViewBoxRow['place_id'];
1401                                                                         $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1402                                                                 }
1403                                                         }
1404                                                         //var_Dump($aPlaceIDs);
1405                                                         //exit;
1406
1407                                                         //now search for housenumber, if housenumber provided
1408                                                         if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1409                                                         {
1410                                                                 $aRoadPlaceIDs = $aPlaceIDs;
1411                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1412
1413                                                                 // Now they are indexed look for a house attached to a street we found
1414                                                                 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1415                                                                 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
1416                                                                 if (sizeof($this->aExcludePlaceIDs))
1417                                                                 {
1418                                                                         $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1419                                                                 }
1420                                                                 $sSQL .= " limit $this->iLimit";
1421                                                                 if (CONST_Debug) var_dump($sSQL);
1422                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1423
1424                                                                 // If nothing found try the aux fallback table
1425                                                                 if (!sizeof($aPlaceIDs))
1426                                                                 {
1427                                                                         $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1428                                                                         if (sizeof($this->aExcludePlaceIDs))
1429                                                                         {
1430                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1431                                                                         }
1432                                                                         //$sSQL .= " limit $this->iLimit";
1433                                                                         if (CONST_Debug) var_dump($sSQL);
1434                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1435                                                                 }
1436                                 //if nothing was found in placex or location_property_aux, then search in Tiger data for this housenumber(location_property_tiger_line)
1437                                                                 if (!sizeof($aPlaceIDs))
1438                                                                 {
1439                                                                         //$sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1440                                                                         //new query for lines, not housenumbers anymore
1441                                     $this->searchedHousenumber = intval($aSearch['sHouseNumber']);
1442                                     if($this->searchedHousenumber%2==0){
1443                                         //if housenumber is even, look for housenumber in streets with interpolationtype even or all
1444                                         $sSQL = "select distinct place_id from location_property_tiger_line where parent_place_id in (".$sPlaceIDs.") and (interpolationtype='even' or interpolationtype='all') and ".$this->searchedHousenumber.">=startnumber and ".$this->searchedHousenumber."<=endnumber";
1445                                     }else{
1446                                         //look for housenumber in streets with interpolationtype odd or all
1447                                         $sSQL = "select distinct place_id from location_property_tiger_line where parent_place_id in (".$sPlaceIDs.") and (interpolationtype='odd' or interpolationtype='all') and ".$this->searchedHousenumber.">=startnumber and ".$this->searchedHousenumber."<=endnumber";
1448                                     }
1449                     
1450                                     if (sizeof($this->aExcludePlaceIDs))
1451                                                                         {
1452                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1453                                                                         }
1454                                                                         //$sSQL .= " limit $this->iLimit";
1455                                                                         if (CONST_Debug) var_dump($sSQL);
1456                                     //get place IDs
1457                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL,0);
1458                                                                 }
1459
1460                                                                 // Fallback to the road (if no housenumber was found)
1461                                                                 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1462                                                                 {
1463                                                                         $aPlaceIDs = $aRoadPlaceIDs;
1464                                     //set to false, if no housenumbers were found
1465                                     $this->housenumberFound=false;
1466                                                                 }else{
1467                                     //housenumber was found
1468                                     $this->housenumberFound=true;
1469                                 }
1470                                                         }
1471
1472
1473                                                         if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1474                                                         {
1475                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1476                                                                 $aClassPlaceIDs = array();
1477
1478                                                                 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1479                                                                 {
1480                                                                         // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1481                                                                         $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1482                                                                         $sSQL .= " and linked_place_id is null";
1483                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1484                                                                         $sSQL .= " order by rank_search asc limit $this->iLimit";
1485                                                                         if (CONST_Debug) var_dump($sSQL);
1486                                                                         $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1487                                                                 }
1488
1489                                                                 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1490                                                                 {
1491                                                                         $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1492                                                                         $bCacheTable = $this->oDB->getOne($sSQL);
1493
1494                                                                         $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1495
1496                                                                         if (CONST_Debug) var_dump($sSQL);
1497                                                                         $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1498
1499                                                                         // For state / country level searches the normal radius search doesn't work very well
1500                                                                         $sPlaceGeom = false;
1501                                                                         if ($this->iMaxRank < 9 && $bCacheTable)
1502                                                                         {
1503                                                                                 // Try and get a polygon to search in instead
1504                                                                                 $sSQL = "select geometry from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank + 5 and st_geometrytype(geometry) in ('ST_Polygon','ST_MultiPolygon') order by rank_search asc limit 1";
1505                                                                                 if (CONST_Debug) var_dump($sSQL);
1506                                                                                 $sPlaceGeom = $this->oDB->getOne($sSQL);
1507                                                                         }
1508
1509                                                                         if ($sPlaceGeom)
1510                                                                         {
1511                                                                                 $sPlaceIDs = false;
1512                                                                         }
1513                                                                         else
1514                                                                         {
1515                                                                                 $this->iMaxRank += 5;
1516                                                                                 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1517                                                                                 if (CONST_Debug) var_dump($sSQL);
1518                                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1519                                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1520                                                                         }
1521
1522                                                                         if ($sPlaceIDs || $sPlaceGeom)
1523                                                                         {
1524
1525                                                                                 $fRange = 0.01;
1526                                                                                 if ($bCacheTable)
1527                                                                                 {
1528                                                                                         // More efficient - can make the range bigger
1529                                                                                         $fRange = 0.05;
1530
1531                                                                                         $sOrderBySQL = '';
1532                                                                                         if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1533                                                                                         else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1534                                                                                         else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1535
1536                                                                                         $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1537                                                                                         if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1538                                                                                         if ($sPlaceIDs)
1539                                                                                         {
1540                                                                                                 $sSQL .= ",placex as f where ";
1541                                                                                                 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1542                                                                                         }
1543                                                                                         if ($sPlaceGeom)
1544                                                                                         {
1545                                                                                                 $sSQL .= " where ";
1546                                                                                                 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1547                                                                                         }
1548                                                                                         if (sizeof($this->aExcludePlaceIDs))
1549                                                                                         {
1550                                                                                                 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1551                                                                                         }
1552                                                                                         if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1553                                                                                         if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1554                                                                                         if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1555                                                                                         $sSQL .= " limit $this->iLimit";
1556                                                                                         if (CONST_Debug) var_dump($sSQL);
1557                                                                                         $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1558                                                                                 }
1559                                                                                 else
1560                                                                                 {
1561                                                                                         if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1562
1563                                                                                         $sOrderBySQL = '';
1564                                                                                         if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1565                                                                                         else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1566
1567                                                                                         $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1568                                                                                         $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1569                                                                                         $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1570                                                                                         if (sizeof($this->aExcludePlaceIDs))
1571                                                                                         {
1572                                                                                                 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1573                                                                                         }
1574                                                                                         if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1575                                                                                         if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1576                                                                                         if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1577                                                                                         $sSQL .= " limit $this->iLimit";
1578                                                                                         if (CONST_Debug) var_dump($sSQL);
1579                                                                                         $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1580                                                                                 }
1581                                                                         }
1582                                                                 }
1583
1584                                                                 $aPlaceIDs = $aClassPlaceIDs;
1585
1586                                                         }
1587
1588                                                 }
1589
1590                                                 if (PEAR::IsError($aPlaceIDs))
1591                                                 {
1592                                                         failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1593                                                 }
1594
1595                                                 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1596
1597                                                 foreach($aPlaceIDs as $iPlaceID)
1598                                                 {
1599                                                         $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1600                                                 }
1601                                                 if ($iQueryLoop > 20) break;
1602                                         }
1603
1604                                         if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1605                                         {
1606                                                 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1607                                                 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1608                                                 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1609                                                 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1610                                                 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1611                                                 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1612                                                 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1613                                                 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1614                                                 $sSQL .= ")";
1615                                                 if (CONST_Debug) var_dump($sSQL);
1616                                                 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1617                                         }
1618
1619                                         //exit;
1620                                         if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1621                                         if ($iGroupLoop > 4) break;
1622                                         if ($iQueryLoop > 30) break;
1623                                 }
1624
1625                                 // Did we find anything?
1626                                 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1627                                 {
1628                                         $aSearchResults = $this->getDetails($aResultPlaceIDs);
1629                                 }
1630
1631                         }
1632                         else
1633                         {
1634                                 // Just interpret as a reverse geocode
1635                                 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1636                                 if ($iPlaceID)
1637                                         $aSearchResults = $this->getDetails(array($iPlaceID));
1638                                 else
1639                                         $aSearchResults = array();
1640                         }
1641
1642                         // No results? Done
1643                         if (!sizeof($aSearchResults))
1644                         {
1645                                 if ($this->bFallback)
1646                                 {
1647                                         if ($this->fallbackStructuredQuery())
1648                                         {
1649                                                 return $this->lookup();
1650                                         }
1651                                 }
1652
1653                                 return array();
1654                         }
1655
1656                         $aClassType = getClassTypesWithImportance();
1657                         $aRecheckWords = preg_split('/\b[\s,\\-]*/u',$sQuery);
1658                         foreach($aRecheckWords as $i => $sWord)
1659                         {
1660                                 if (!preg_match('/\pL/', $sWord)) unset($aRecheckWords[$i]);
1661                         }
1662
1663                         if (CONST_Debug) { echo '<i>Recheck words:<\i>'; var_dump($aRecheckWords); }
1664
1665                         foreach($aSearchResults as $iResNum => $aResult)
1666                         {
1667                                 // Default
1668                                 $fDiameter = getResultDiameter($aResult);
1669
1670                                 $oPlaceLookup = new PlaceLookup($this->oDB);
1671                                 $oPlaceLookup->setIncludePolygonAsPoints($this->bIncludePolygonAsPoints);
1672                                 $oPlaceLookup->setIncludePolygonAsText($this->bIncludePolygonAsText);
1673                                 $oPlaceLookup->setIncludePolygonAsGeoJSON($this->bIncludePolygonAsGeoJSON);
1674                                 $oPlaceLookup->setIncludePolygonAsKML($this->bIncludePolygonAsKML);
1675                                 $oPlaceLookup->setIncludePolygonAsSVG($this->bIncludePolygonAsSVG);
1676                                 $oPlaceLookup->setPolygonSimplificationThreshold($this->fPolygonSimplificationThreshold);
1677
1678                                 $aOutlineResult = $oPlaceLookup->getOutlines($aResult['place_id'], $aResult['lon'], $aResult['lat'], $fDiameter/2);
1679                                 if ($aOutlineResult)
1680                                 {
1681                                         $aResult = array_merge($aResult, $aOutlineResult);
1682                                 }
1683                                 
1684                                 if ($aResult['extra_place'] == 'city')
1685                                 {
1686                                         $aResult['class'] = 'place';
1687                                         $aResult['type'] = 'city';
1688                                         $aResult['rank_search'] = 16;
1689                                 }
1690
1691                                 // Is there an icon set for this type of result?
1692                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1693                                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1694                                 {
1695                                         $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1696                                 }
1697
1698                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1699                                                 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1700                                 {
1701                                         $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1702                                 }
1703                                 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1704                                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1705                                 {
1706                                         $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1707                                 }
1708                 /* Implement this function later. if tag '&addressdetails=1' is set in query
1709                                 if ($this->bIncludeAddressDetails)
1710                                 {
1711                 *   getAddressDetails() is defined in lib.php and uses the SQL function get_addressdata in functions.sql
1712                                         $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1713                                         if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1714                                         {
1715                                                 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1716                                         }
1717                                 }
1718                 */
1719                                 if ($this->bIncludeExtraTags)
1720                                 {
1721                                         if ($aResult['extra'])
1722                                         {
1723                                                 $aResult['sExtraTags'] = json_decode($aResult['extra']);
1724                                         }
1725                                         else
1726                                         {
1727                                                 $aResult['sExtraTags'] = (object) array();
1728                                         }
1729                                 }
1730
1731                                 if ($this->bIncludeNameDetails)
1732                                 {
1733                                         if ($aResult['names'])
1734                                         {
1735                                                 $aResult['sNameDetails'] = json_decode($aResult['names']);
1736                                         }
1737                                         else
1738                                         {
1739                                                 $aResult['sNameDetails'] = (object) array();
1740                                         }
1741                                 }
1742
1743                                 // Adjust importance for the number of exact string matches in the result
1744                                 $aResult['importance'] = max(0.001,$aResult['importance']);
1745                                 $iCountWords = 0;
1746                                 $sAddress = $aResult['langaddress'];
1747                                 foreach($aRecheckWords as $i => $sWord)
1748                                 {
1749                                         if (stripos($sAddress, $sWord)!==false)
1750                                         {
1751                                                 $iCountWords++;
1752                                                 if (preg_match("/(^|,)\s*".preg_quote($sWord, '/')."\s*(,|$)/", $sAddress)) $iCountWords += 0.1;
1753                                         }
1754                                 }
1755
1756                                 $aResult['importance'] = $aResult['importance'] + ($iCountWords*0.1); // 0.1 is a completely arbitrary number but something in the range 0.1 to 0.5 would seem right
1757
1758                                 $aResult['name'] = $aResult['langaddress'];
1759                                 // secondary ordering (for results with same importance (the smaller the better):
1760                                 //   - approximate importance of address parts
1761                                 $aResult['foundorder'] = -$aResult['addressimportance']/10;
1762                                 //   - number of exact matches from the query
1763                                 if (isset($this->exactMatchCache[$aResult['place_id']]))
1764                                         $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1765                                 else if (isset($this->exactMatchCache[$aResult['parent_place_id']]))
1766                                         $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1767                                 //  - importance of the class/type
1768                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1769                                         && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1770                                 {
1771                                         $aResult['foundorder'] += 0.0001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1772                                 }
1773                                 else
1774                                 {
1775                                         $aResult['foundorder'] += 0.01;
1776                                 }
1777                                 if (CONST_Debug) { var_dump($aResult); }
1778                                 $aSearchResults[$iResNum] = $aResult;
1779                         }
1780                         uasort($aSearchResults, 'byImportance');
1781
1782                         $aOSMIDDone = array();
1783                         $aClassTypeNameDone = array();
1784                         $aToFilter = $aSearchResults;
1785                         $aSearchResults = array();
1786
1787                         $bFirst = true;
1788                         foreach($aToFilter as $iResNum => $aResult)
1789                         {
1790                                 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1791                                 if ($bFirst)
1792                                 {
1793                                         $fLat = $aResult['lat'];
1794                                         $fLon = $aResult['lon'];
1795                                         if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1796                                         $bFirst = false;
1797                                 }
1798                                 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1799                                                         && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1800                                 {
1801                                         $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1802                                         $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1803                                         $aSearchResults[] = $aResult;
1804                                 }
1805
1806                                 // Absolute limit on number of results
1807                                 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1808                         }
1809
1810                         return $aSearchResults;
1811
1812                 } // end lookup()
1813
1814
1815         } // end class
1816