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