| OLD | NEW |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "content/renderer/android/address_detector.h" | 5 #include "content/common/android/address_parser_internal.h" |
| 6 | 6 |
| 7 #include <bitset> | 7 #include <bitset> |
| 8 | 8 |
| 9 #include "base/logging.h" | 9 #include "base/logging.h" |
| 10 #include "base/string_util.h" | 10 #include "base/string_util.h" |
| 11 #include "base/utf_string_conversions.h" | |
| 12 #include "net/base/escape.h" | |
| 13 #include "third_party/WebKit/Source/WebKit/chromium/public/platform/WebString.h" | |
| 14 | 11 |
| 15 namespace { | 12 namespace { |
| 16 | 13 |
| 17 // Prefix used for geographical address intent URIs. | |
| 18 static const char kAddressSchemaPrefix[] = "geo:0,0?q="; | |
| 19 | |
| 20 // Maximum text length to be searched for address detection. | |
| 21 static const size_t kMaxAddressLength = 250; | |
| 22 | |
| 23 // Minimum number of words in an address after the house number | |
| 24 // before a state is expected to be found. | |
| 25 // A value too high can miss short addresses. | |
| 26 const size_t kMinAddressWords = 3; | |
| 27 | |
| 28 // Maximum number of words allowed in an address between the house number | |
| 29 // and the state, both not included. | |
| 30 const size_t kMaxAddressWords = 12; | |
| 31 | |
| 32 // Maximum number of lines allowed in an address between the house number | |
| 33 // and the state, both not included. | |
| 34 const size_t kMaxAddressLines = 5; | |
| 35 | |
| 36 // Maximum length allowed for any address word between the house number | |
| 37 // and the state, both not included. | |
| 38 const size_t kMaxAddressNameWordLength = 25; | |
| 39 | |
| 40 // Maximum number of words after the house number in which the location name | |
| 41 // should be found. | |
| 42 const size_t kMaxLocationNameDistance = 4; | |
| 43 | |
| 44 // Number of digits for a valid zip code. | 14 // Number of digits for a valid zip code. |
| 45 const size_t kZipDigits = 5; | 15 const size_t kZipDigits = 5; |
| 46 | 16 |
| 47 // Number of digits for a valid zip code in the Zip Plus 4 format. | 17 // Number of digits for a valid zip code in the Zip Plus 4 format. |
| 48 const size_t kZipPlus4Digits = 9; | 18 const size_t kZipPlus4Digits = 9; |
| 49 | 19 |
| 50 // Maximum number of digits of a house number, including possible hyphens. | 20 // Maximum number of digits of a house number, including possible hyphens. |
| 51 const size_t kMaxHouseDigits = 5; | 21 const size_t kMaxHouseDigits = 5; |
| 52 | 22 |
| 53 // Additional characters used as new line delimiters. | |
| 54 const char16 kNewlineDelimiters[] = { | |
| 55 '\n', | |
| 56 ',', | |
| 57 '*', | |
| 58 0x2022, // Unicode bullet | |
| 59 0, | |
| 60 }; | |
| 61 | |
| 62 char16 SafePreviousChar(const string16::const_iterator& it, | 23 char16 SafePreviousChar(const string16::const_iterator& it, |
| 63 const string16::const_iterator& begin) { | 24 const string16::const_iterator& begin) { |
| 64 if (it == begin) | 25 if (it == begin) |
| 65 return ' '; | 26 return ' '; |
| 66 return *(it - 1); | 27 return *(it - 1); |
| 67 } | 28 } |
| 68 | 29 |
| 69 char16 SafeNextChar(const string16::const_iterator& it, | 30 char16 SafeNextChar(const string16::const_iterator& it, |
| 70 const string16::const_iterator& end) { | 31 const string16::const_iterator& end) { |
| 71 if (it == end) | 32 if (it == end) |
| (...skipping 22 matching lines...) Expand all Loading... |
| 94 if (!*ascii_to_match || base::ToLowerASCII(*it) != *ascii_to_match) | 55 if (!*ascii_to_match || base::ToLowerASCII(*it) != *ascii_to_match) |
| 95 return false; | 56 return false; |
| 96 } | 57 } |
| 97 return *ascii_to_match == 0; | 58 return *ascii_to_match == 0; |
| 98 } | 59 } |
| 99 | 60 |
| 100 } // anonymous namespace | 61 } // anonymous namespace |
| 101 | 62 |
| 102 namespace content { | 63 namespace content { |
| 103 | 64 |
| 104 AddressDetector::AddressDetector() { | 65 namespace address_parser { |
| 105 } | |
| 106 | 66 |
| 107 AddressDetector::~AddressDetector() { | 67 namespace internal { |
| 108 } | |
| 109 | 68 |
| 110 std::string AddressDetector::GetContentText(const string16& text) { | 69 Word::Word(const string16::const_iterator& begin, |
| 111 // Get the address and replace unicode bullets with commas. | 70 const string16::const_iterator& end) |
| 112 string16 address_16 = CollapseWhitespace(text, false); | |
| 113 std::replace(address_16.begin(), address_16.end(), | |
| 114 static_cast<char16>(0x2022), static_cast<char16>(',')); | |
| 115 return UTF16ToUTF8(address_16); | |
| 116 } | |
| 117 | |
| 118 GURL AddressDetector::GetIntentURL(const std::string& content_text) { | |
| 119 return GURL(kAddressSchemaPrefix + | |
| 120 net::EscapeQueryParamValue(content_text, true)); | |
| 121 } | |
| 122 | |
| 123 size_t AddressDetector::GetMaximumContentLength() { | |
| 124 return kMaxAddressLength; | |
| 125 } | |
| 126 | |
| 127 bool AddressDetector::FindContent(const string16::const_iterator& begin, | |
| 128 const string16::const_iterator& end, | |
| 129 size_t* start_pos, | |
| 130 size_t* end_pos, | |
| 131 std::string* content_text) { | |
| 132 HouseNumberParser house_number_parser; | |
| 133 | |
| 134 // Keep going through the input string until a potential house number is | |
| 135 // detected. Start tokenizing the following words to find a valid | |
| 136 // street name within a word range. Then, find a state name followed | |
| 137 // by a valid zip code for that state. Also keep a look for any other | |
| 138 // possible house numbers to continue from in case of no match and for | |
| 139 // state names not followed by a zip code (e.g. New York, NY 10000). | |
| 140 const string16 newline_delimiters = kNewlineDelimiters; | |
| 141 const string16 delimiters = kWhitespaceUTF16 + newline_delimiters; | |
| 142 for (string16::const_iterator it = begin; it != end; ) { | |
| 143 Word house_number; | |
| 144 if (!house_number_parser.Parse(it, end, &house_number)) | |
| 145 return false; | |
| 146 | |
| 147 String16Tokenizer tokenizer(house_number.end, end, delimiters); | |
| 148 tokenizer.set_options(String16Tokenizer::RETURN_DELIMS); | |
| 149 | |
| 150 std::vector<Word> words; | |
| 151 words.push_back(house_number); | |
| 152 | |
| 153 bool found_location_name = false; | |
| 154 bool continue_on_house_number = true; | |
| 155 bool consecutive_house_numbers = true; | |
| 156 size_t next_house_number_word = 0; | |
| 157 size_t num_lines = 1; | |
| 158 | |
| 159 // Don't include the house number in the word count. | |
| 160 size_t next_word = 1; | |
| 161 for (; next_word <= kMaxAddressWords + 1; ++next_word) { | |
| 162 | |
| 163 // Extract a new word from the tokenizer. | |
| 164 if (next_word == words.size()) { | |
| 165 do { | |
| 166 if (!tokenizer.GetNext()) | |
| 167 return false; | |
| 168 | |
| 169 // Check the number of address lines. | |
| 170 if (tokenizer.token_is_delim() && newline_delimiters.find( | |
| 171 *tokenizer.token_begin()) != string16::npos) { | |
| 172 ++num_lines; | |
| 173 } | |
| 174 } while (tokenizer.token_is_delim()); | |
| 175 | |
| 176 if (num_lines > kMaxAddressLines) | |
| 177 break; | |
| 178 | |
| 179 words.push_back(Word(tokenizer.token_begin(), tokenizer.token_end())); | |
| 180 } | |
| 181 | |
| 182 // Check the word length. If too long, don't try to continue from | |
| 183 // the next house number as no address can hold this word. | |
| 184 const Word& current_word = words[next_word]; | |
| 185 DCHECK_GT(std::distance(current_word.begin, current_word.end), 0); | |
| 186 size_t current_word_length = std::distance( | |
| 187 current_word.begin, current_word.end); | |
| 188 if (current_word_length > kMaxAddressNameWordLength) { | |
| 189 continue_on_house_number = false; | |
| 190 break; | |
| 191 } | |
| 192 | |
| 193 // Check if the new word is a valid house number. | |
| 194 if (house_number_parser.Parse(current_word.begin, current_word.end, | |
| 195 NULL)) { | |
| 196 // Increase the number of consecutive house numbers since the beginning. | |
| 197 if (consecutive_house_numbers) { | |
| 198 // Check if there is a new line between consecutive house numbers. | |
| 199 // This avoids false positives of the form "Cafe 21\n 750 Fifth Ave.." | |
| 200 if (num_lines > 1) { | |
| 201 next_house_number_word = next_word; | |
| 202 break; | |
| 203 } | |
| 204 } | |
| 205 | |
| 206 // Keep the next candidate to resume parsing from in case of failure. | |
| 207 if (next_house_number_word == 0) { | |
| 208 next_house_number_word = next_word; | |
| 209 continue; | |
| 210 } | |
| 211 } else { | |
| 212 consecutive_house_numbers = false; | |
| 213 } | |
| 214 | |
| 215 // Look for location names in the words after the house number. | |
| 216 // A range limitation is introduced to avoid matching | |
| 217 // anything that starts with a number before a legitimate address. | |
| 218 if (next_word <= kMaxLocationNameDistance && | |
| 219 IsValidLocationName(current_word)) { | |
| 220 found_location_name = true; | |
| 221 continue; | |
| 222 } | |
| 223 | |
| 224 // Don't count the house number. | |
| 225 if (next_word > kMinAddressWords) { | |
| 226 // Looking for the state is likely to add new words to the list while | |
| 227 // checking for multi-word state names. | |
| 228 size_t state_first_word = next_word; | |
| 229 size_t state_last_word, state_index; | |
| 230 if (FindStateStartingInWord(&words, state_first_word, &state_last_word, | |
| 231 &tokenizer, &state_index)) { | |
| 232 | |
| 233 // A location name should have been found at this point. | |
| 234 if (!found_location_name) | |
| 235 break; | |
| 236 | |
| 237 // Explicitly exclude "et al", as "al" is a valid state code. | |
| 238 if (current_word_length == 2 && words.size() > 2) { | |
| 239 const Word& previous_word = words[state_first_word - 1]; | |
| 240 if (previous_word.end - previous_word.begin == 2 && | |
| 241 LowerCaseEqualsASCII(previous_word.begin, previous_word.end, | |
| 242 "et") && | |
| 243 LowerCaseEqualsASCII(current_word.begin, current_word.end, | |
| 244 "al")) | |
| 245 break; | |
| 246 } | |
| 247 | |
| 248 // Extract one more word from the tokenizer if not already available. | |
| 249 size_t zip_word = state_last_word + 1; | |
| 250 if (zip_word == words.size()) { | |
| 251 do { | |
| 252 if (!tokenizer.GetNext()) | |
| 253 return false; | |
| 254 } while (tokenizer.token_is_delim()); | |
| 255 words.push_back(Word(tokenizer.token_begin(), | |
| 256 tokenizer.token_end())); | |
| 257 } | |
| 258 | |
| 259 // Check the parsing validity and state range of the zip code. | |
| 260 next_word = state_last_word; | |
| 261 if (!IsZipValid(words[zip_word], state_index)) | |
| 262 continue; | |
| 263 | |
| 264 *start_pos = words[0].begin - begin; | |
| 265 *end_pos = words[zip_word].end - begin; | |
| 266 content_text->assign(GetContentText(string16(words[0].begin, | |
| 267 words[zip_word].end))); | |
| 268 return true; | |
| 269 } | |
| 270 } | |
| 271 } | |
| 272 | |
| 273 // Avoid skipping too many words because of a non-address number | |
| 274 // at the beginning of the contents to parse. | |
| 275 if (continue_on_house_number && next_house_number_word > 0) { | |
| 276 it = words[next_house_number_word].begin; | |
| 277 } else { | |
| 278 DCHECK(!words.empty()); | |
| 279 next_word = std::min(next_word, words.size() - 1); | |
| 280 it = words[next_word].end; | |
| 281 } | |
| 282 } | |
| 283 | |
| 284 return false; | |
| 285 } | |
| 286 | |
| 287 AddressDetector::Word::Word(const string16::const_iterator& begin, | |
| 288 const string16::const_iterator& end) | |
| 289 : begin(begin), | 71 : begin(begin), |
| 290 end(end) { | 72 end(end) { |
| 291 DCHECK(begin <= end); | 73 DCHECK(begin <= end); |
| 292 } | 74 } |
| 293 | 75 |
| 294 bool AddressDetector::HouseNumberParser::IsPreDelimiter( | 76 bool HouseNumberParser::IsPreDelimiter(char16 character) { |
| 295 char16 character) { | |
| 296 return character == ':' || IsPostDelimiter(character); | 77 return character == ':' || IsPostDelimiter(character); |
| 297 } | 78 } |
| 298 | 79 |
| 299 bool AddressDetector::HouseNumberParser::IsPostDelimiter( | 80 bool HouseNumberParser::IsPostDelimiter(char16 character) { |
| 300 char16 character) { | |
| 301 return IsWhitespace(character) || strchr(",\"'", character); | 81 return IsWhitespace(character) || strchr(",\"'", character); |
| 302 } | 82 } |
| 303 | 83 |
| 304 void AddressDetector::HouseNumberParser::RestartOnNextDelimiter() { | 84 void HouseNumberParser::RestartOnNextDelimiter() { |
| 305 ResetState(); | 85 ResetState(); |
| 306 for (; it_ != end_ && !IsPreDelimiter(*it_); ++it_) {} | 86 for (; it_ != end_ && !IsPreDelimiter(*it_); ++it_) {} |
| 307 } | 87 } |
| 308 | 88 |
| 309 void AddressDetector::HouseNumberParser::AcceptChars(size_t num_chars) { | 89 void HouseNumberParser::AcceptChars(size_t num_chars) { |
| 310 size_t offset = std::min(static_cast<size_t>(std::distance(it_, end_)), | 90 size_t offset = std::min(static_cast<size_t>(std::distance(it_, end_)), |
| 311 num_chars); | 91 num_chars); |
| 312 it_ += offset; | 92 it_ += offset; |
| 313 result_chars_ += offset; | 93 result_chars_ += offset; |
| 314 } | 94 } |
| 315 | 95 |
| 316 void AddressDetector::HouseNumberParser::SkipChars(size_t num_chars) { | 96 void HouseNumberParser::SkipChars(size_t num_chars) { |
| 317 it_ += std::min(static_cast<size_t>(std::distance(it_, end_)), num_chars); | 97 it_ += std::min(static_cast<size_t>(std::distance(it_, end_)), num_chars); |
| 318 } | 98 } |
| 319 | 99 |
| 320 void AddressDetector::HouseNumberParser::ResetState() { | 100 void HouseNumberParser::ResetState() { |
| 321 num_digits_ = 0; | 101 num_digits_ = 0; |
| 322 result_chars_ = 0; | 102 result_chars_ = 0; |
| 323 } | 103 } |
| 324 | 104 |
| 325 bool AddressDetector::HouseNumberParser::CheckFinished(Word* word) const { | 105 bool HouseNumberParser::CheckFinished(Word* word) const { |
| 326 // There should always be a number after a hyphen. | 106 // There should always be a number after a hyphen. |
| 327 if (result_chars_ == 0 || SafePreviousChar(it_, begin_) == '-') | 107 if (result_chars_ == 0 || SafePreviousChar(it_, begin_) == '-') |
| 328 return false; | 108 return false; |
| 329 | 109 |
| 330 if (word) { | 110 if (word) { |
| 331 word->begin = it_ - result_chars_; | 111 word->begin = it_ - result_chars_; |
| 332 word->end = it_; | 112 word->end = it_; |
| 333 } | 113 } |
| 334 return true; | 114 return true; |
| 335 } | 115 } |
| 336 | 116 |
| 337 bool AddressDetector::HouseNumberParser::Parse( | 117 bool HouseNumberParser::Parse( |
| 338 const string16::const_iterator& begin, | 118 const string16::const_iterator& begin, |
| 339 const string16::const_iterator& end, Word* word) { | 119 const string16::const_iterator& end, Word* word) { |
| 340 it_ = begin_ = begin; | 120 it_ = begin_ = begin; |
| 341 end_ = end; | 121 end_ = end; |
| 342 ResetState(); | 122 ResetState(); |
| 343 | 123 |
| 344 // Iterations only used as a fail-safe against any buggy infinite loops. | 124 // Iterations only used as a fail-safe against any buggy infinite loops. |
| 345 size_t iterations = 0; | 125 size_t iterations = 0; |
| 346 size_t max_iterations = end - begin + 1; | 126 size_t max_iterations = end - begin + 1; |
| 347 for (; it_ != end_ && iterations < max_iterations; ++iterations) { | 127 for (; it_ != end_ && iterations < max_iterations; ++iterations) { |
| (...skipping 111 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 459 RestartOnNextDelimiter(); | 239 RestartOnNextDelimiter(); |
| 460 SkipChars(1); | 240 SkipChars(1); |
| 461 } | 241 } |
| 462 | 242 |
| 463 if (iterations >= max_iterations) | 243 if (iterations >= max_iterations) |
| 464 return false; | 244 return false; |
| 465 | 245 |
| 466 return CheckFinished(word); | 246 return CheckFinished(word); |
| 467 } | 247 } |
| 468 | 248 |
| 469 bool AddressDetector::FindStateStartingInWord( | 249 bool FindStateStartingInWord(WordList* words, |
| 470 WordList* words, | 250 size_t state_first_word, |
| 471 size_t state_first_word, | 251 size_t* state_last_word, |
| 472 size_t* state_last_word, | 252 String16Tokenizer* tokenizer, |
| 473 String16Tokenizer* tokenizer, | 253 size_t* state_index) { |
| 474 size_t* state_index) { | |
| 475 | 254 |
| 476 // Bitmasks containing the allowed suffixes for 2-letter state codes. | 255 // Bitmasks containing the allowed suffixes for 2-letter state codes. |
| 477 static const int state_two_letter_suffix[23] = { | 256 static const int state_two_letter_suffix[23] = { |
| 478 0x02060c00, // A followed by: [KLRSZ]. | 257 0x02060c00, // A followed by: [KLRSZ]. |
| 479 0x00000000, // B. | 258 0x00000000, // B. |
| 480 0x00084001, // C followed by: [AOT]. | 259 0x00084001, // C followed by: [AOT]. |
| 481 0x00000014, // D followed by: [CE]. | 260 0x00000014, // D followed by: [CE]. |
| 482 0x00000000, // E. | 261 0x00000000, // E. |
| 483 0x00001800, // F followed by: [LM]. | 262 0x00001800, // F followed by: [LM]. |
| 484 0x00100001, // G followed by: [AU]. | 263 0x00100001, // G followed by: [AU]. |
| (...skipping 141 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 626 if (state_match) { | 405 if (state_match) { |
| 627 *state_last_word = state_word; | 406 *state_last_word = state_word; |
| 628 *state_index = state_names[state].state_index; | 407 *state_index = state_names[state].state_index; |
| 629 return true; | 408 return true; |
| 630 } | 409 } |
| 631 } | 410 } |
| 632 | 411 |
| 633 return false; | 412 return false; |
| 634 } | 413 } |
| 635 | 414 |
| 636 bool AddressDetector::IsZipValid(const Word& word, size_t state_index) { | 415 bool IsZipValid(const Word& word, size_t state_index) { |
| 637 size_t length = word.end - word.begin; | 416 size_t length = word.end - word.begin; |
| 638 if (length != kZipDigits && length != kZipPlus4Digits + 1) | 417 if (length != kZipDigits && length != kZipPlus4Digits + 1) |
| 639 return false; | 418 return false; |
| 640 | 419 |
| 641 for (string16::const_iterator it = word.begin; it != word.end; ++it) { | 420 for (string16::const_iterator it = word.begin; it != word.end; ++it) { |
| 642 size_t pos = it - word.begin; | 421 size_t pos = it - word.begin; |
| 643 if (IsAsciiDigit(*it) || (*it == '-' && pos == kZipDigits)) | 422 if (IsAsciiDigit(*it) || (*it == '-' && pos == kZipDigits)) |
| 644 continue; | 423 continue; |
| 645 return false; | 424 return false; |
| 646 } | 425 } |
| 647 return IsZipValidForState(word, state_index); | 426 return IsZipValidForState(word, state_index); |
| 648 } | 427 } |
| 649 | 428 |
| 650 bool AddressDetector::IsZipValidForState(const Word& word, size_t state_index) { | 429 bool IsZipValidForState(const Word& word, size_t state_index) { |
| 651 // List of valid zip code ranges. | 430 // List of valid zip code ranges. |
| 652 static const struct { | 431 static const struct { |
| 653 char low; | 432 char low; |
| 654 char high; | 433 char high; |
| 655 char exception1; | 434 char exception1; |
| 656 char exception2; | 435 char exception2; |
| 657 } zip_range[] = { | 436 } zip_range[] = { |
| 658 { 99, 99, -1, -1 }, // AK Alaska. | 437 { 99, 99, -1, -1 }, // AK Alaska. |
| 659 { 35, 36, -1, -1 }, // AL Alabama. | 438 { 35, 36, -1, -1 }, // AL Alabama. |
| 660 { 71, 72, -1, -1 }, // AR Arkansas. | 439 { 71, 72, -1, -1 }, // AR Arkansas. |
| (...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 724 | 503 |
| 725 if ((zip_prefix >= zip_range[state_index].low && | 504 if ((zip_prefix >= zip_range[state_index].low && |
| 726 zip_prefix <= zip_range[state_index].high) || | 505 zip_prefix <= zip_range[state_index].high) || |
| 727 zip_prefix == zip_range[state_index].exception1 || | 506 zip_prefix == zip_range[state_index].exception1 || |
| 728 zip_prefix == zip_range[state_index].exception2) { | 507 zip_prefix == zip_range[state_index].exception2) { |
| 729 return true; | 508 return true; |
| 730 } | 509 } |
| 731 return false; | 510 return false; |
| 732 } | 511 } |
| 733 | 512 |
| 734 bool AddressDetector::IsValidLocationName(const Word& word) { | 513 bool IsValidLocationName(const Word& word) { |
| 735 // Supported location names sorted alphabetically and grouped by first letter. | 514 // Supported location names sorted alphabetically and grouped by first letter. |
| 736 static const struct LocationNameInfo { | 515 static const struct LocationNameInfo { |
| 737 const char* string; | 516 const char* string; |
| 738 char length; | 517 char length; |
| 739 bool allow_plural; | 518 bool allow_plural; |
| 740 } location_names[157] = { | 519 } location_names[157] = { |
| 741 { "alley", 5, false }, { "annex", 5, false }, { "arcade", 6, false }, | 520 { "alley", 5, false }, { "annex", 5, false }, { "arcade", 6, false }, |
| 742 { "ave", 3, false }, { "ave.", 4, false }, { "avenue", 6, false }, | 521 { "ave", 3, false }, { "ave.", 4, false }, { "avenue", 6, false }, |
| 743 { "alameda", 7, false }, | 522 { "alameda", 7, false }, |
| 744 { "bayou", 5, false }, { "beach", 5, false }, { "bend", 4, false }, | 523 { "bayou", 5, false }, { "beach", 5, false }, { "bend", 4, false }, |
| (...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 835 if (LowerCaseEqualsASCIIWithPlural(word.begin, word.end, | 614 if (LowerCaseEqualsASCIIWithPlural(word.begin, word.end, |
| 836 location_names[i].string, | 615 location_names[i].string, |
| 837 location_names[i].allow_plural)) { | 616 location_names[i].allow_plural)) { |
| 838 return true; | 617 return true; |
| 839 } | 618 } |
| 840 } | 619 } |
| 841 | 620 |
| 842 return false; | 621 return false; |
| 843 } | 622 } |
| 844 | 623 |
| 624 } // namespace internal |
| 625 |
| 626 } // namespace address_parser |
| 627 |
| 845 } // namespace content | 628 } // namespace content |
| OLD | NEW |