crossref-matcher 1.2.1__tar.gz → 1.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {crossref_matcher-1.2.1/crossref_matcher.egg-info → crossref_matcher-1.2.2}/PKG-INFO +1 -1
- crossref_matcher-1.2.2/crossref_matcher/resources/VERSION.txt +1 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/plugins.json +0 -1
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/funder/funder_name_to_ror_search/strategy.py +40 -2
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2/crossref_matcher.egg-info}/PKG-INFO +1 -1
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher.egg-info/SOURCES.txt +0 -4
- crossref_matcher-1.2.1/crossref_matcher/resources/VERSION.txt +0 -1
- crossref_matcher-1.2.1/crossref_matcher/strategies/funder/funder_single_search_01/strategy.py +0 -481
- crossref_matcher-1.2.1/crossref_matcher/strategies/funder/funder_single_search_02/strategy.py +0 -520
- crossref_matcher-1.2.1/crossref_matcher/strategies/reference/sbmv_orig/__init__.py +0 -0
- crossref_matcher-1.2.1/crossref_matcher/strategies/reference/sbmv_unstructured/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/LICENSE +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/MANIFEST.in +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/README.md +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/MatchTask.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/app.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/all_links.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/api_routes.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/dataset_ingest.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/evaluation.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/google_sheets_connector.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/ror_utils.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/evaluation/schemas.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/preprint_matching/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/preprint_matching/indexer_service.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/ror_organizations/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/ror_organizations/create_index.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/ror_organizations/index_data.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/ror_organizations/ror_opensearch_update_dag.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/indexes/ror_organizations/ror_zenodo_api.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/matching/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/matching/http_utils.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/matching/run_strategy_on_dataset.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/matching/schemas.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/matching/search.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/matching/utils.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/affiliation/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/common_ror_endings/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/common_ror_endings/common_ror_endings.txt +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/countries/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/countries/china_placenames.txt +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/countries/countries.txt +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/countries/countries2.txt +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/datasets/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/data_counters0.pkl +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/eval/events.out.tfevents.1690572611.knopfler.local +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.opt_slots0.npy.gz +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.pkl.gz +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/model.weights.npy.gz +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ensemble_ml/train/events.out.tfevents.1690572611.knopfler.local +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ml/data_counters0.pkl +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ml/eval/events.out.tfevents.1690302892.knopfler.local +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ml/model.opt_slots0.npy.gz +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ml/model.pkl.gz +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ml/model.weights.npy.gz +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/preprint_ml/train/events.out.tfevents.1690302891.knopfler.local +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/data_counters0.pkl +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/eval/events.out.tfevents.1681511550.knopfler.local +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.opt_slots0.npy.gz +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.pkl.gz +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/model.weights.npy.gz +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/resources/data/models/reference_nn_unstructured/train/events.out.tfevents.1681511550.knopfler.local +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/run_plugin_test.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/es_utils.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/matching.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/multi_search/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/multi_search/strategy.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/single_search/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/single_search/strategy.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/affiliation/strategy.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/common.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/funder/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/funder/funder_name_to_ror_search/__init__.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/funder/funder_single_search_01 → crossref_matcher-1.2.2/crossref_matcher/strategies/healthcheck}/__init__.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/funder/funder_single_search_02 → crossref_matcher-1.2.2/crossref_matcher/strategies/healthcheck/healthcheck}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/healthcheck/healthcheck/strategy.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/healthcheck → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint}/__init__.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/healthcheck/healthcheck → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/ensemble_avg}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/ensemble_avg/strategy.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/ensemble_cascade}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/ensemble_cascade/strategy.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint/ensemble_avg → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/ensemble_ml}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/ensemble_ml/get_train_data.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/ensemble_ml/strategy.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/ensemble_ml/train.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/ensemble_ml/utils.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint/ensemble_cascade → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/preprint_ml}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/preprint_ml/get_train_data.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/preprint_ml/strategy.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/preprint_ml/train.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/preprint_ml/utils.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint/ensemble_ml → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/sbmv}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/sbmv/strategy.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint/preprint_ml → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/sbmv_es}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/sbmv_es/strategy.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint/sbmv → crossref_matcher-1.2.2/crossref_matcher/strategies/preprint/sbmv_rels}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/preprint/sbmv_rels/strategy.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint/sbmv_es → crossref_matcher-1.2.2/crossref_matcher/strategies/reference}/__init__.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/preprint/sbmv_rels → crossref_matcher-1.2.2/crossref_matcher/strategies/reference/cs_combined}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/cs_combined/strategy.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/reference → crossref_matcher-1.2.2/crossref_matcher/strategies/reference/nn_unstructured}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/nn_unstructured/get_train_data.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/nn_unstructured/strategy.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/nn_unstructured/train.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/nn_unstructured/utils.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/reference/cs_combined → crossref_matcher-1.2.2/crossref_matcher/strategies/reference/parsed_doi}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/parsed_doi/strategy.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/reference/nn_unstructured → crossref_matcher-1.2.2/crossref_matcher/strategies/reference/parsed_isbn}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/parsed_isbn/strategy.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/reference/parsed_doi → crossref_matcher-1.2.2/crossref_matcher/strategies/reference/sbmv}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/sbmv/strategy.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/reference/parsed_isbn → crossref_matcher-1.2.2/crossref_matcher/strategies/reference/sbmv_orig}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/sbmv_orig/strategy.py +0 -0
- {crossref_matcher-1.2.1/crossref_matcher/strategies/reference/sbmv → crossref_matcher-1.2.2/crossref_matcher/strategies/reference/sbmv_unstructured}/__init__.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher/strategies/reference/sbmv_unstructured/strategy.py +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher.egg-info/dependency_links.txt +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher.egg-info/entry_points.txt +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher.egg-info/requires.txt +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/crossref_matcher.egg-info/top_level.txt +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/setup.cfg +0 -0
- {crossref_matcher-1.2.1 → crossref_matcher-1.2.2}/setup.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.2.2
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
"crossref_matcher.strategies.reference.sbmv",
|
|
12
12
|
"crossref_matcher.strategies.reference.sbmv_orig",
|
|
13
13
|
"crossref_matcher.strategies.reference.sbmv_unstructured",
|
|
14
|
-
"crossref_matcher.strategies.funder.funder_single_search_01",
|
|
15
14
|
"crossref_matcher.strategies.funder.funder_name_to_ror_search",
|
|
16
15
|
"crossref_example_matching_strategy"
|
|
17
16
|
],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import re
|
|
3
|
-
from crossref_matcher.matching.utils import get_resource_path
|
|
4
3
|
from typing import Iterable
|
|
4
|
+
from crossref_matcher.matching.utils import get_resource_path
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
import unidecode
|
|
7
7
|
from crossref_matcher.strategies import Strategy, MatchTask
|
|
@@ -53,6 +53,7 @@ class FunderNameToRorSearch(Strategy):
|
|
|
53
53
|
index = "organization"
|
|
54
54
|
min_score = 96
|
|
55
55
|
|
|
56
|
+
# Matches acronyms: sequences of 2 or more consecutive uppercase ASCII letters as whole words (e.g., 'NASA', 'UNESCO').
|
|
56
57
|
pattern_acronym = re.compile(r"(?P<acronym>\b[A-Z]{2,}\b)")
|
|
57
58
|
|
|
58
59
|
def __init__(self, es_client=ES_CLIENT, max_candidates=18, extra_output=False):
|
|
@@ -117,6 +118,12 @@ class FunderNameToRorSearch(Strategy):
|
|
|
117
118
|
def match(self, input_data):
|
|
118
119
|
self.input_string_rep = StringRep(input_data)
|
|
119
120
|
self.fund_countries = self.get_countries(self.input_string_rep)
|
|
121
|
+
|
|
122
|
+
# Before anything, check for exact acronym match (very conservatively)
|
|
123
|
+
acronym_result = self.acronym_substrategy_match(self.input_string_rep)
|
|
124
|
+
if acronym_result:
|
|
125
|
+
return [acronym_result]
|
|
126
|
+
|
|
120
127
|
candidates = self.get_candidates(input_data, max_candidates=self.max_candidates)
|
|
121
128
|
# store initial candidates for help with debugging/evaluation
|
|
122
129
|
self.initial_candidates = [
|
|
@@ -200,7 +207,14 @@ class FunderNameToRorSearch(Strategy):
|
|
|
200
207
|
)["hits"]["hits"]
|
|
201
208
|
|
|
202
209
|
def _exclusion_acronym_common_ending(self, fund: StringRep, name: StringRep):
|
|
203
|
-
|
|
210
|
+
"""
|
|
211
|
+
If the ROR name (`name`) being considered ends in a common ending
|
|
212
|
+
(e.g., "foundation", "research center") and contains an acronym
|
|
213
|
+
(e.g., "NASA"), then exclude it if the acronym(s) do not also occur
|
|
214
|
+
in the fund name (`fund`).
|
|
215
|
+
|
|
216
|
+
Returns True if this `name` should be excluded.
|
|
217
|
+
"""
|
|
204
218
|
acronyms = self.pattern_acronym.findall(name.original)
|
|
205
219
|
if not acronyms:
|
|
206
220
|
return False
|
|
@@ -416,3 +430,27 @@ class FunderNameToRorSearch(Strategy):
|
|
|
416
430
|
def get_countries(self, s: StringRep) -> list[str]:
|
|
417
431
|
codes = self.get_country_codes(s)
|
|
418
432
|
return [self.to_region(c) for c in codes]
|
|
433
|
+
|
|
434
|
+
def acronym_substrategy_match(self, s: StringRep) -> dict | None:
|
|
435
|
+
stripped = s.original.strip()
|
|
436
|
+
whitelist = ["CNPq"]
|
|
437
|
+
# Must be 1 word, all uppercase, and 4 or more letters
|
|
438
|
+
if re.fullmatch(r"[A-Z]{4,}", stripped) or stripped in whitelist:
|
|
439
|
+
r = self.es_client.search(
|
|
440
|
+
body={
|
|
441
|
+
"size": 10,
|
|
442
|
+
"query": {
|
|
443
|
+
"bool": {"filter": {"term": {"acronyms": {"value": stripped}}}}
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
index=self.index,
|
|
447
|
+
)["hits"]["hits"]
|
|
448
|
+
|
|
449
|
+
# there must be one and only one match
|
|
450
|
+
if r and len(r) == 1:
|
|
451
|
+
return {
|
|
452
|
+
"id": r[0]["_id"],
|
|
453
|
+
"confidence": 1,
|
|
454
|
+
"strategies": [self.strategy],
|
|
455
|
+
}
|
|
456
|
+
return None
|
|
@@ -80,10 +80,6 @@ crossref_matcher/strategies/affiliation/single_search/strategy.py
|
|
|
80
80
|
crossref_matcher/strategies/funder/__init__.py
|
|
81
81
|
crossref_matcher/strategies/funder/funder_name_to_ror_search/__init__.py
|
|
82
82
|
crossref_matcher/strategies/funder/funder_name_to_ror_search/strategy.py
|
|
83
|
-
crossref_matcher/strategies/funder/funder_single_search_01/__init__.py
|
|
84
|
-
crossref_matcher/strategies/funder/funder_single_search_01/strategy.py
|
|
85
|
-
crossref_matcher/strategies/funder/funder_single_search_02/__init__.py
|
|
86
|
-
crossref_matcher/strategies/funder/funder_single_search_02/strategy.py
|
|
87
83
|
crossref_matcher/strategies/healthcheck/__init__.py
|
|
88
84
|
crossref_matcher/strategies/healthcheck/healthcheck/__init__.py
|
|
89
85
|
crossref_matcher/strategies/healthcheck/healthcheck/strategy.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.2.1
|
crossref_matcher-1.2.1/crossref_matcher/strategies/funder/funder_single_search_01/strategy.py
DELETED
|
@@ -1,481 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
from crossref_matcher.matching.utils import get_resource_path
|
|
4
|
-
from typing import Iterable
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
import unidecode
|
|
7
|
-
import unicodedata
|
|
8
|
-
from functools import cached_property
|
|
9
|
-
from crossref_matcher.strategies import Strategy, MatchTask
|
|
10
|
-
|
|
11
|
-
from crossref_matcher.matching.search import ES_CLIENT
|
|
12
|
-
|
|
13
|
-
from rapidfuzz import fuzz
|
|
14
|
-
|
|
15
|
-
import logging
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def matches_exactly(name, substring, ignore_case=True):
|
|
21
|
-
if ignore_case:
|
|
22
|
-
try:
|
|
23
|
-
name = name.lower_
|
|
24
|
-
except AttributeError:
|
|
25
|
-
name = name.lower()
|
|
26
|
-
substring = substring.lower()
|
|
27
|
-
return re.search(rf"\b{re.escape(substring)}\b", name) is not None
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class StringRep(str):
|
|
31
|
-
# This is meant to standardize all of the manipulations of the name strings
|
|
32
|
-
def __new__(cls, s: str):
|
|
33
|
-
obj = super().__new__(cls, s)
|
|
34
|
-
obj.original = str(s)
|
|
35
|
-
return obj
|
|
36
|
-
|
|
37
|
-
def __str__(self):
|
|
38
|
-
return self.original
|
|
39
|
-
|
|
40
|
-
def __repr__(self):
|
|
41
|
-
return f"StringRep({self.original!r})"
|
|
42
|
-
|
|
43
|
-
def __len__(self):
|
|
44
|
-
return len(self.original)
|
|
45
|
-
|
|
46
|
-
@cached_property
|
|
47
|
-
def lower_(self):
|
|
48
|
-
return re.sub(r"\s+", " ", self.original.lower())
|
|
49
|
-
|
|
50
|
-
@cached_property
|
|
51
|
-
def lower_alpha(self):
|
|
52
|
-
s = unidecode.unidecode(self.original).strip()
|
|
53
|
-
return re.sub(
|
|
54
|
-
r"\s+", " ", re.sub("[^a-z]", " ", s.lower())
|
|
55
|
-
) # Lowercase and keep only letters
|
|
56
|
-
|
|
57
|
-
@cached_property
|
|
58
|
-
def alpha(self):
|
|
59
|
-
s = unidecode.unidecode(self.original).strip()
|
|
60
|
-
return re.sub(r"\s+", " ", re.sub("[^a-zA-Z]", " ", s)) # Keep only letters
|
|
61
|
-
|
|
62
|
-
def check_latin_chars(self, s):
|
|
63
|
-
for ch in s:
|
|
64
|
-
if ch.isalpha():
|
|
65
|
-
if "LATIN" not in unicodedata.name(ch):
|
|
66
|
-
return False
|
|
67
|
-
return True
|
|
68
|
-
|
|
69
|
-
@cached_property
|
|
70
|
-
def normalized(self):
|
|
71
|
-
return self._normalize(self.original)
|
|
72
|
-
|
|
73
|
-
def _normalize(self, s):
|
|
74
|
-
"""Normalize a string for matching.
|
|
75
|
-
|
|
76
|
-
Args:
|
|
77
|
-
s (str): The string to normalize.
|
|
78
|
-
|
|
79
|
-
Returns:
|
|
80
|
-
str: The normalized string.
|
|
81
|
-
"""
|
|
82
|
-
if self.check_latin_chars(s):
|
|
83
|
-
# if these are Latin characters, remove accent marks
|
|
84
|
-
s = re.sub(r"\s+", " ", unidecode.unidecode(s).strip().lower())
|
|
85
|
-
else:
|
|
86
|
-
s = re.sub(r"\s+", " ", s.strip().lower())
|
|
87
|
-
|
|
88
|
-
# Replace specific abbreviations with their full forms
|
|
89
|
-
s = re.sub(
|
|
90
|
-
"(?<![a-z])univ$",
|
|
91
|
-
"university",
|
|
92
|
-
re.sub(
|
|
93
|
-
r"(?<![a-z])univ[\. ]",
|
|
94
|
-
"university ",
|
|
95
|
-
re.sub(r"(?<![a-z])u\.(?! ?[a-z]\.)", "university ", s),
|
|
96
|
-
),
|
|
97
|
-
)
|
|
98
|
-
s = re.sub(
|
|
99
|
-
"(?<![a-z])lab$",
|
|
100
|
-
"laboratory",
|
|
101
|
-
re.sub("(?<![a-z])lab[^a-z]", "laboratory ", s),
|
|
102
|
-
)
|
|
103
|
-
s = re.sub(
|
|
104
|
-
"(?<![a-z])inst$",
|
|
105
|
-
"institute",
|
|
106
|
-
re.sub("(?<![a-z])inst[^a-z]", "institute ", s),
|
|
107
|
-
)
|
|
108
|
-
s = re.sub(
|
|
109
|
-
"(?<![a-z])tech$",
|
|
110
|
-
"technology",
|
|
111
|
-
re.sub("(?<![a-z])tech[^a-z]", "technology ", s),
|
|
112
|
-
)
|
|
113
|
-
s = re.sub(r"(?<![a-z])u\. ?s\.", "united states", s)
|
|
114
|
-
s = re.sub("&", " and ", re.sub("&", " and ", s))
|
|
115
|
-
s = re.sub("^the ", "", s)
|
|
116
|
-
|
|
117
|
-
s = re.sub(r"\s+", " ", s.strip().lower())
|
|
118
|
-
return s
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
@dataclass
|
|
122
|
-
class CandidateMatch:
|
|
123
|
-
# Represents a candidate match in the search
|
|
124
|
-
candidate: dict
|
|
125
|
-
name: StringRep
|
|
126
|
-
score: float
|
|
127
|
-
start: int
|
|
128
|
-
end: int
|
|
129
|
-
|
|
130
|
-
def trimmed_output(self):
|
|
131
|
-
return {
|
|
132
|
-
"ror_id": self.candidate["_id"],
|
|
133
|
-
"name": self.name.original,
|
|
134
|
-
"score": self.score,
|
|
135
|
-
"start": self.start,
|
|
136
|
-
"end": self.end,
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
class FunderSingleSearch01(Strategy):
|
|
141
|
-
id = "funder-single-search-01"
|
|
142
|
-
task = MatchTask.FUNDER
|
|
143
|
-
description = "Heuristic-based strategy; it performs a single candidate search against ES and uses a number of heuristics to validate the candidates."
|
|
144
|
-
|
|
145
|
-
index = "organization"
|
|
146
|
-
min_score = 96
|
|
147
|
-
|
|
148
|
-
pattern_acronym = re.compile(r"(?P<acronym>\b[A-Z]{2,}\b)")
|
|
149
|
-
|
|
150
|
-
def __init__(self, es_client=ES_CLIENT, max_candidates=18):
|
|
151
|
-
self.es_client = es_client
|
|
152
|
-
self.max_candidates = max_candidates
|
|
153
|
-
if self.es_client is None:
|
|
154
|
-
if os.getenv("PYTEST_CURRENT_TEST"):
|
|
155
|
-
logger.warning(
|
|
156
|
-
"this strategy normally requires an elasticsearch client, but pytest is running, so bypassing this error"
|
|
157
|
-
)
|
|
158
|
-
else:
|
|
159
|
-
raise RuntimeError("this strategy requires an elasticsearch client")
|
|
160
|
-
|
|
161
|
-
self.countries = []
|
|
162
|
-
with open(
|
|
163
|
-
get_resource_path(
|
|
164
|
-
"crossref_matcher.resources.data.countries", "countries2.txt"
|
|
165
|
-
),
|
|
166
|
-
"r",
|
|
167
|
-
encoding="utf-8",
|
|
168
|
-
) as file:
|
|
169
|
-
lines = [
|
|
170
|
-
line.strip().split()
|
|
171
|
-
for line in file
|
|
172
|
-
if not line.startswith("#") and line.strip()
|
|
173
|
-
]
|
|
174
|
-
self.countries = [(line[0], " ".join(line[1:])) for line in lines]
|
|
175
|
-
|
|
176
|
-
self.common_ror_endings = set()
|
|
177
|
-
with open(
|
|
178
|
-
get_resource_path(
|
|
179
|
-
"crossref_matcher.resources.data.common_ror_endings",
|
|
180
|
-
"common_ror_endings.txt",
|
|
181
|
-
),
|
|
182
|
-
"r",
|
|
183
|
-
encoding="utf-8",
|
|
184
|
-
) as file:
|
|
185
|
-
for line in file:
|
|
186
|
-
line = line.strip()
|
|
187
|
-
if line:
|
|
188
|
-
self.common_ror_endings.add(line)
|
|
189
|
-
|
|
190
|
-
self.fund_countries = [] # can be filled in during match()
|
|
191
|
-
self.initial_candidates = [] # can be filled in during match()
|
|
192
|
-
# self.word_counter = Counter() # can be filled in during match()
|
|
193
|
-
|
|
194
|
-
def match(self, input_data, extra=False):
|
|
195
|
-
self.input_string_rep = StringRep(input_data)
|
|
196
|
-
self.fund_countries = self.get_countries(self.input_string_rep)
|
|
197
|
-
candidates = self.get_candidates(input_data, max_candidates=self.max_candidates)
|
|
198
|
-
# store initial candidates for help with debugging/evaluation
|
|
199
|
-
self.initial_candidates = [
|
|
200
|
-
{"id": c["_id"], "elasticsearch_score": c["_score"]} for c in candidates
|
|
201
|
-
]
|
|
202
|
-
candidates = [c for c in candidates if self.eligible_candidates_filter(c)]
|
|
203
|
-
|
|
204
|
-
# self.word_counter = Counter()
|
|
205
|
-
# for candidate in candidates:
|
|
206
|
-
# this_candidate_words = set()
|
|
207
|
-
# for candidate_name in candidate["_source"]["names"]:
|
|
208
|
-
# name = candidate_name["name"]
|
|
209
|
-
# # count each word only once per candidate
|
|
210
|
-
# this_candidate_words.update(re.split(r"\b", name.lower()))
|
|
211
|
-
# self.word_counter.update(this_candidate_words)
|
|
212
|
-
# common_words = set(word for word, freq in self.word_counter.most_common() if freq > 1 and len(word) > 1)
|
|
213
|
-
|
|
214
|
-
candidates = [self.score(self.input_string_rep, c) for c in candidates]
|
|
215
|
-
all_scored_candidates = candidates
|
|
216
|
-
candidates = [c for c in candidates if c.score >= self.min_score]
|
|
217
|
-
|
|
218
|
-
matched = self.choose_candidate(self.input_string_rep, candidates)
|
|
219
|
-
if matched is None:
|
|
220
|
-
return []
|
|
221
|
-
if (
|
|
222
|
-
self.fund_countries
|
|
223
|
-
and self.to_region(matched.candidate["_source"]["country"])
|
|
224
|
-
not in self.fund_countries
|
|
225
|
-
):
|
|
226
|
-
return []
|
|
227
|
-
|
|
228
|
-
ret = {
|
|
229
|
-
"id": matched.candidate["_id"],
|
|
230
|
-
"confidence": min(12, matched.candidate["_score"]) / 12,
|
|
231
|
-
"strategies": [self.strategy],
|
|
232
|
-
}
|
|
233
|
-
if extra is True:
|
|
234
|
-
candidates_out = [c.trimmed_output() for c in all_scored_candidates]
|
|
235
|
-
candidates_out.sort(key=lambda c: c["score"], reverse=True)
|
|
236
|
-
ret.update(
|
|
237
|
-
{
|
|
238
|
-
"matched_name": matched.name,
|
|
239
|
-
"alignment_score": matched.score,
|
|
240
|
-
"fund_countries": self.fund_countries,
|
|
241
|
-
"all_scored_candidates": candidates_out,
|
|
242
|
-
"elasticsearch_score": matched.candidate["_score"],
|
|
243
|
-
}
|
|
244
|
-
)
|
|
245
|
-
return [ret]
|
|
246
|
-
|
|
247
|
-
def eligible_candidates_filter(self, candidate) -> bool:
|
|
248
|
-
if candidate["_source"]["status"] not in ["active", "inactive"]:
|
|
249
|
-
return False
|
|
250
|
-
elif (
|
|
251
|
-
self.fund_countries
|
|
252
|
-
and self.to_region(candidate["_source"]["country"])
|
|
253
|
-
not in self.fund_countries
|
|
254
|
-
):
|
|
255
|
-
return False
|
|
256
|
-
else:
|
|
257
|
-
return True
|
|
258
|
-
|
|
259
|
-
def get_candidates(self, fund, max_candidates=20):
|
|
260
|
-
return self.es_client.search(
|
|
261
|
-
body={
|
|
262
|
-
"size": max_candidates,
|
|
263
|
-
# "size": 10,
|
|
264
|
-
"query": {
|
|
265
|
-
"nested": {
|
|
266
|
-
"path": "names",
|
|
267
|
-
"score_mode": "max",
|
|
268
|
-
"query": {"match": {"names.name": {"query": fund}}},
|
|
269
|
-
}
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
index=self.index,
|
|
273
|
-
)["hits"]["hits"]
|
|
274
|
-
|
|
275
|
-
def _exclusion_acronym_common_ending(self, fund: StringRep, name: StringRep):
|
|
276
|
-
# returns True if this `name` should be excluded
|
|
277
|
-
acronyms = self.pattern_acronym.findall(name.original)
|
|
278
|
-
if not acronyms:
|
|
279
|
-
return False
|
|
280
|
-
for ending in self.common_ror_endings:
|
|
281
|
-
# use a regex to see if `name` ends in `ending`
|
|
282
|
-
if re.search(rf"\b{re.escape(ending)}$", name.lower_):
|
|
283
|
-
# acronyms from `name` must also occur in `fund` (not fuzzy)
|
|
284
|
-
if not all(matches_exactly(fund, acronym) for acronym in acronyms):
|
|
285
|
-
return True
|
|
286
|
-
return False
|
|
287
|
-
|
|
288
|
-
def candidate_name_match_exclusion(self, fund: StringRep, name: StringRep):
|
|
289
|
-
# returns True if this `name` should be excluded
|
|
290
|
-
if (
|
|
291
|
-
# names that are too general to match:
|
|
292
|
-
name.lower_
|
|
293
|
-
in [
|
|
294
|
-
"university school",
|
|
295
|
-
"university hospital",
|
|
296
|
-
"development fund",
|
|
297
|
-
"korea university", # there are two korea universities (one in japan, one in south korea)
|
|
298
|
-
]
|
|
299
|
-
# names that are too general to match unless a country is matched:
|
|
300
|
-
or (
|
|
301
|
-
not self.fund_countries
|
|
302
|
-
and name.lower_
|
|
303
|
-
in [
|
|
304
|
-
"computing center",
|
|
305
|
-
"center for international studies",
|
|
306
|
-
"central government",
|
|
307
|
-
"centre for international studies",
|
|
308
|
-
"department of education",
|
|
309
|
-
"department of conservation",
|
|
310
|
-
"marie curie",
|
|
311
|
-
"department of the environment",
|
|
312
|
-
"department of veterans affairs",
|
|
313
|
-
"development research center",
|
|
314
|
-
"education trust",
|
|
315
|
-
"heritage fund",
|
|
316
|
-
"hospital research foundation",
|
|
317
|
-
"medical research council",
|
|
318
|
-
"medical research foundation",
|
|
319
|
-
"ministry of agriculture, food and rural affairs",
|
|
320
|
-
"ministry of education",
|
|
321
|
-
"ministry of science",
|
|
322
|
-
"national council of science and technology",
|
|
323
|
-
"national institute of health research",
|
|
324
|
-
"national research institute",
|
|
325
|
-
"national science foundation",
|
|
326
|
-
"national science fund",
|
|
327
|
-
"prostate cancer research",
|
|
328
|
-
"research promotion foundation",
|
|
329
|
-
"startup foundation",
|
|
330
|
-
"the alliance",
|
|
331
|
-
"l’alliance",
|
|
332
|
-
"alliance",
|
|
333
|
-
"the arc",
|
|
334
|
-
"the data lab",
|
|
335
|
-
"the george",
|
|
336
|
-
"the research council",
|
|
337
|
-
"the research network",
|
|
338
|
-
"the royal",
|
|
339
|
-
"the tech",
|
|
340
|
-
"the thousand",
|
|
341
|
-
"the trust",
|
|
342
|
-
"the way",
|
|
343
|
-
"union government",
|
|
344
|
-
"waste management",
|
|
345
|
-
"r core",
|
|
346
|
-
"creative foundation",
|
|
347
|
-
"fonds de la recherche scientifique",
|
|
348
|
-
"fund for scientific research",
|
|
349
|
-
"fonds national de la recherche",
|
|
350
|
-
"foundation for strategic research",
|
|
351
|
-
]
|
|
352
|
-
)
|
|
353
|
-
or len(name) >= len(fund) + 4
|
|
354
|
-
or len(name) < 5
|
|
355
|
-
or (" " not in name.original and fund.lower_ != name.lower_)
|
|
356
|
-
or (" " not in fund.original and fund.lower_ != name.lower_)
|
|
357
|
-
or self._exclusion_acronym_common_ending(fund, name)
|
|
358
|
-
):
|
|
359
|
-
return True
|
|
360
|
-
return False
|
|
361
|
-
|
|
362
|
-
def score(self, fund: StringRep, candidate) -> CandidateMatch:
|
|
363
|
-
best = CandidateMatch(
|
|
364
|
-
candidate=candidate, name=StringRep(""), score=0, start=-1, end=-1
|
|
365
|
-
)
|
|
366
|
-
for candidate_name in candidate["_source"]["names"]:
|
|
367
|
-
name = StringRep(candidate_name["name"])
|
|
368
|
-
if self.candidate_name_match_exclusion(fund, name):
|
|
369
|
-
continue
|
|
370
|
-
alignment = fuzz.partial_ratio_alignment(fund.normalized, name.normalized)
|
|
371
|
-
if alignment.score > best.score:
|
|
372
|
-
best = CandidateMatch(
|
|
373
|
-
candidate=candidate,
|
|
374
|
-
name=name,
|
|
375
|
-
score=alignment.score,
|
|
376
|
-
start=alignment.src_start,
|
|
377
|
-
end=alignment.src_end,
|
|
378
|
-
)
|
|
379
|
-
return best
|
|
380
|
-
|
|
381
|
-
def is_better(
|
|
382
|
-
self, fund: StringRep, candidate: CandidateMatch, other: CandidateMatch
|
|
383
|
-
) -> bool:
|
|
384
|
-
score = 0
|
|
385
|
-
if "univ" in candidate.name.lower_ and "univ" not in other.name.lower_:
|
|
386
|
-
score += 1
|
|
387
|
-
if "univ" not in candidate.name.lower_ and "univ" in other.name.lower_:
|
|
388
|
-
score -= 1
|
|
389
|
-
c_diff = abs(len(candidate.name) - len(fund))
|
|
390
|
-
o_diff = abs(len(other.name) - len(fund))
|
|
391
|
-
if o_diff - c_diff > 4:
|
|
392
|
-
score += 1
|
|
393
|
-
if c_diff - o_diff > 4:
|
|
394
|
-
score -= 1
|
|
395
|
-
if candidate.start > other.end:
|
|
396
|
-
score += 1
|
|
397
|
-
if other.start > candidate.end:
|
|
398
|
-
score -= 1
|
|
399
|
-
if candidate.score > 99 and other.score < 99:
|
|
400
|
-
score += 1
|
|
401
|
-
if candidate.score < 99 and other.score > 99:
|
|
402
|
-
score -= 1
|
|
403
|
-
return score > 0
|
|
404
|
-
|
|
405
|
-
def rescore(self, fund: StringRep, candidates: Iterable[CandidateMatch]):
|
|
406
|
-
new_scores = []
|
|
407
|
-
for candidate in candidates:
|
|
408
|
-
ns = 0
|
|
409
|
-
for other in candidates:
|
|
410
|
-
if self.is_better(fund, candidate, other):
|
|
411
|
-
ns += 1
|
|
412
|
-
new_scores.append(ns)
|
|
413
|
-
for c, ns in zip(candidates, new_scores):
|
|
414
|
-
c.score = ns
|
|
415
|
-
yield c
|
|
416
|
-
|
|
417
|
-
def last_non_overlapping(self, candidates):
|
|
418
|
-
matched = None
|
|
419
|
-
for candidate in candidates:
|
|
420
|
-
overlap = False
|
|
421
|
-
for other in candidates:
|
|
422
|
-
if candidate.candidate["_id"] == other.candidate["_id"]:
|
|
423
|
-
continue
|
|
424
|
-
if (
|
|
425
|
-
candidate.start <= other.start <= candidate.end
|
|
426
|
-
or candidate.start <= other.end <= candidate.end
|
|
427
|
-
or other.start <= candidate.start <= other.end
|
|
428
|
-
or other.start <= candidate.end <= other.end
|
|
429
|
-
):
|
|
430
|
-
overlap = True
|
|
431
|
-
if not overlap:
|
|
432
|
-
matched = candidate
|
|
433
|
-
return matched
|
|
434
|
-
|
|
435
|
-
def choose_candidate(self, fund: StringRep, candidates: Iterable[CandidateMatch]):
|
|
436
|
-
if not candidates:
|
|
437
|
-
return None
|
|
438
|
-
|
|
439
|
-
if len(candidates) == 1:
|
|
440
|
-
return candidates[0]
|
|
441
|
-
|
|
442
|
-
rescored = list(self.rescore(fund, candidates))
|
|
443
|
-
top_score = max([c.score for c in rescored])
|
|
444
|
-
top_scored = [c for c in rescored if c.score == top_score]
|
|
445
|
-
|
|
446
|
-
if len(top_scored) == 1:
|
|
447
|
-
return top_scored[0]
|
|
448
|
-
|
|
449
|
-
return self.last_non_overlapping(top_scored)
|
|
450
|
-
|
|
451
|
-
def to_region(self, c: str) -> str:
|
|
452
|
-
return {
|
|
453
|
-
"GB": "GB-UK",
|
|
454
|
-
"UK": "GB-UK",
|
|
455
|
-
"CN": "CN-HK-TW",
|
|
456
|
-
"HK": "CN-HK-TW",
|
|
457
|
-
"TW": "CN-HK-TW",
|
|
458
|
-
"PR": "US-PR",
|
|
459
|
-
"US": "US-PR",
|
|
460
|
-
}.get(c, c)
|
|
461
|
-
|
|
462
|
-
def get_country_codes(self, s: StringRep) -> list[str]:
|
|
463
|
-
_lower = re.sub(r"\s+", " ", unidecode.unidecode(s.original).strip().lower())
|
|
464
|
-
codes = []
|
|
465
|
-
for code, name in self.countries:
|
|
466
|
-
if re.search("[^a-z]", name):
|
|
467
|
-
# name contains not only letters (e.g., a space)
|
|
468
|
-
score = fuzz.partial_ratio(name, _lower)
|
|
469
|
-
elif len(name) == 2:
|
|
470
|
-
score = max(
|
|
471
|
-
[fuzz.ratio(name.upper(), t) for t in s.alpha.split()] + [0]
|
|
472
|
-
)
|
|
473
|
-
else:
|
|
474
|
-
score = max([fuzz.ratio(name, t) for t in s.lower_alpha.split()] + [0])
|
|
475
|
-
if score >= 90:
|
|
476
|
-
codes.append(code.upper())
|
|
477
|
-
return list(set(codes))
|
|
478
|
-
|
|
479
|
-
def get_countries(self, s: StringRep) -> list[str]:
|
|
480
|
-
codes = self.get_country_codes(s)
|
|
481
|
-
return [self.to_region(c) for c in codes]
|