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