emanuelaboros commited on
Commit
74fdc85
·
1 Parent(s): be0c2cf

Make NEL pipeline generic for historical entity linking

Browse files
.idea/.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
4
+ # Ignored default folder with query files
5
+ /queries/
6
+ # Datasource local storage ignored files
7
+ /dataSources/
8
+ /dataSources.local.xml
9
+ # Editor-based HTTP Client requests
10
+ /httpRequests/
.idea/copilot.data.migration.agent.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="AgentMigrationStateService">
4
+ <option name="migrationStatus" value="COMPLETED" />
5
+ </component>
6
+ </project>
.idea/historical-nel.iml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <excludeFolder url="file://$MODULE_DIR$/.venv" />
6
+ </content>
7
+ <orderEntry type="jdk" jdkName="Python 3.14 (historical-nel)" jdkType="Python SDK" />
8
+ <orderEntry type="sourceFolder" forTests="false" />
9
+ </component>
10
+ <component name="PyDocumentationSettings">
11
+ <option name="format" value="PLAIN" />
12
+ <option name="myDocStringFormat" value="Plain" />
13
+ </component>
14
+ </module>
.idea/inspectionProfiles/Project_Default.xml ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <profile version="1.0">
3
+ <option name="myName" value="Project Default" />
4
+ <inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
5
+ <Languages>
6
+ <language minSize="73" name="Python" />
7
+ </Languages>
8
+ </inspection_tool>
9
+ <inspection_tool class="PyPackageRequirementsInspection" enabled="false" level="WARNING" enabled_by_default="false">
10
+ <option name="ignoredPackages">
11
+ <list>
12
+ <option value="beautifulsoup4" />
13
+ <option value="pycld2" />
14
+ <option value="colorama" />
15
+ <option value="epub" />
16
+ <option value="evaluate" />
17
+ <option value="huggingface_hub" />
18
+ <option value="dask" />
19
+ <option value="sentence_transformers" />
20
+ <option value="jsonschema" />
21
+ <option value="importlib_resources" />
22
+ <option value="sentence-transformers" />
23
+ <option value="jsonlines" />
24
+ <option value="langdetect" />
25
+ <option value="pandas" />
26
+ <option value="pybloomfiltermmap3" />
27
+ <option value="pysbd" />
28
+ <option value="python-dotenv" />
29
+ <option value="PyYAML" />
30
+ <option value="scikit_learn" />
31
+ <option value="spacy" />
32
+ <option value="tiktoken" />
33
+ <option value="tqdm" />
34
+ <option value="trl" />
35
+ <option value="boto3" />
36
+ <option value="s3fs" />
37
+ <option value="jiwer" />
38
+ <option value="dkpro-cassis" />
39
+ <option value="dvc" />
40
+ <option value="dvc_s3" />
41
+ <option value="chonkie" />
42
+ <option value="levenshtein" />
43
+ <option value="pillow" />
44
+ <option value="openai" />
45
+ </list>
46
+ </option>
47
+ </inspection_tool>
48
+ <inspection_tool class="PyTupleAssignmentBalanceInspection" enabled="false" level="WARNING" enabled_by_default="false" />
49
+ </profile>
50
+ </component>
.idea/inspectionProfiles/profiles_settings.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
.idea/misc.xml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Black">
4
+ <option name="sdkName" value="Python 3.14 (historical-nel)" />
5
+ </component>
6
+ <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14 (historical-nel)" project-jdk-type="Python SDK" />
7
+ </project>
.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/historical-nel.iml" filepath="$PROJECT_DIR$/.idea/historical-nel.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
generic_nel.py CHANGED
@@ -1,6 +1,5 @@
1
  from transformers import Pipeline
2
  import requests
3
- import torch
4
  import time
5
  import os
6
  import numpy as np
@@ -16,16 +15,14 @@ logger = logging.getLogger("nel_wikidata")
16
 
17
  NEL_MODEL = "nel-mgenre-multilingual"
18
 
19
- HEADERS = {
20
- "User-Agent": "NEL-HF-Space/0.1 (contact: emanuela.boros@univ-lr.fr)"
21
- }
22
 
23
  WIKIMEDIA_QPS = float(os.getenv("WIKIMEDIA_QPS", "3.0")) # total across ALL processes
24
  _MIN_INTERVAL = 1.0 / max(WIKIMEDIA_QPS, 0.1)
25
 
26
  _LOCK = FileLock(os.getenv("WIKIMEDIA_RATELOCK", "/tmp/wikimedia_rate.lock"))
27
  _TS_FILE = os.getenv("WIKIMEDIA_RATETS", "/tmp/wikimedia_rate.ts")
28
-
29
  requests_cache.install_cache(
30
  cache_name=os.getenv("WIKIMEDIA_CACHE", "wikimedia_cache"),
31
  backend="sqlite",
@@ -44,7 +41,7 @@ _retry = Retry(
44
  backoff_factor=float(os.getenv("WIKIMEDIA_BACKOFF", "1.5")),
45
  status_forcelist=(429, 500, 502, 503, 504),
46
  allowed_methods=frozenset(["GET"]),
47
- respect_retry_after_header=False, # <-- IMPORTANT
48
  raise_on_status=False,
49
  )
50
 
@@ -52,6 +49,7 @@ _adapter = HTTPAdapter(max_retries=_retry, pool_connections=50, pool_maxsize=50)
52
  _session.mount("https://", _adapter)
53
  _session.mount("http://", _adapter)
54
 
 
55
  def safe_get(url, params, retries=3, backoff=2):
56
  for attempt in range(retries):
57
  try:
@@ -59,7 +57,7 @@ def safe_get(url, params, retries=3, backoff=2):
59
  if resp.status_code == 200:
60
  return resp
61
  elif resp.status_code in (403, 429): # forbidden or rate limited
62
- wait = backoff ** attempt
63
  print(f"Retrying {url} in {wait}s (status={resp.status_code})")
64
  time.sleep(wait)
65
  continue
@@ -67,7 +65,7 @@ def safe_get(url, params, retries=3, backoff=2):
67
  resp.raise_for_status()
68
  except Exception as e:
69
  print(f"Request error: {e}")
70
- time.sleep(backoff ** attempt)
71
  return None
72
 
73
 
@@ -106,6 +104,7 @@ def get_claim_strings(claims, prop):
106
  values.append(v["time"])
107
  return values
108
 
 
109
  def extract_datavalue(claim):
110
  try:
111
  return claim["mainsnak"]["datavalue"]["value"]
@@ -135,6 +134,7 @@ def get_claim_string_values(claims, prop):
135
  vals.append(v["time"])
136
  return vals
137
 
 
138
  def get_claim_coordinates(claims, prop="P625"):
139
  """
140
  Return coordinates from Wikidata if present.
@@ -143,11 +143,13 @@ def get_claim_coordinates(claims, prop="P625"):
143
  for c in claims.get(prop, []):
144
  v = extract_datavalue(c)
145
  if isinstance(v, dict) and "latitude" in v and "longitude" in v:
146
- coords.append({
147
- "latitude": v["latitude"],
148
- "longitude": v["longitude"],
149
- "precision": v.get("precision")
150
- })
 
 
151
  return coords
152
 
153
 
@@ -227,11 +229,15 @@ def safe_get_old(url, params, timeout=10):
227
  # Even with Retry-After handling, sometimes you still get here.
228
  ra = resp.headers.get("Retry-After")
229
  sleep = float(ra) if (ra and ra.isdigit()) else (1.0 + random.random())
230
- logger.warning(f"[WIKI] 429 rate limited. retry-after={ra} sleeping={sleep:.2f}s url={url}")
 
 
231
  time.sleep(sleep)
232
  return resp # caller may treat as failure; Retry already happened.
233
 
234
- logger.warning(f"[WIKI] HTTP {resp.status_code} url={url} params={_short(params)}")
 
 
235
  return resp
236
  except Exception as e:
237
  logger.exception(f"[WIKI] Request error: {e} url={url}")
@@ -242,187 +248,33 @@ def _short(params: dict, maxlen: int = 120) -> str:
242
  s = str(params)
243
  return s if len(s) <= maxlen else s[:maxlen] + "..."
244
 
245
- # def get_wikidata_details(qid, language="en"):
246
- # """
247
- # Return richer Wikidata + Wikipedia details for a QID.
248
- # """
249
- # if not qid or qid == "NIL":
250
- # return {
251
- # "wikidata_label": "NIL",
252
- # "wikidata_description": None,
253
- # "wikidata_aliases": [],
254
- # "wikidata_summary": None,
255
- # "wikipedia_title": "NIL",
256
- # "wikipedia_url": "None",
257
- # "wikipedia_summary": None,
258
- # "entity_type": "UNK",
259
- # "instance_of": [],
260
- # "occupations": [],
261
- # "country": [],
262
- # "date_of_birth": None,
263
- # "date_of_death": None,
264
- # "coordinates": [],
265
- # }
266
-
267
- # # ---- Wikidata entity data ----
268
- # wd_url = "https://www.wikidata.org/w/api.php"
269
- # wd_params = {
270
- # "action": "wbgetentities",
271
- # "format": "json",
272
- # "ids": qid,
273
- # "props": "labels|descriptions|aliases|sitelinks/urls|claims",
274
- # "languages": language,
275
- # "sitefilter": f"{language}wiki",
276
- # }
277
-
278
- # resp = safe_get(wd_url, wd_params)
279
- # if not resp:
280
- # return {
281
- # "wikidata_label": qid,
282
- # "wikidata_description": None,
283
- # "wikidata_aliases": [],
284
- # "wikidata_summary": None,
285
- # "wikipedia_title": "NIL",
286
- # "wikipedia_url": "None",
287
- # "wikipedia_summary": None,
288
- # "entity_type": "UNK",
289
- # "instance_of": [],
290
- # "occupations": [],
291
- # "country": [],
292
- # "date_of_birth": None,
293
- # "date_of_death": None,
294
- # "coordinates": [],
295
- # }
296
-
297
- # try:
298
- # data = resp.json()
299
- # entity = data["entities"][qid]
300
-
301
- # labels = entity.get("labels", {})
302
- # descriptions = entity.get("descriptions", {})
303
- # aliases = entity.get("aliases", {})
304
- # sitelinks = entity.get("sitelinks", {})
305
- # claims = entity.get("claims", {})
306
-
307
- # wikidata_label = (
308
- # labels.get(language, {}).get("value")
309
- # or labels.get("en", {}).get("value")
310
- # or qid
311
- # )
312
-
313
- # wikidata_description = (
314
- # descriptions.get(language, {}).get("value")
315
- # or descriptions.get("en", {}).get("value")
316
- # )
317
-
318
- # wikidata_aliases = [
319
- # a["value"]
320
- # for a in aliases.get(language, []) or aliases.get("en", [])
321
- # ]
322
-
323
- # if f"{language}wiki" in sitelinks:
324
- # wikipedia_title = sitelinks[f"{language}wiki"]["title"]
325
- # wikipedia_url = sitelinks[f"{language}wiki"]["url"]
326
- # else:
327
- # wikipedia_title, wikipedia_url = "NIL", "None"
328
-
329
- # # coarse type
330
- # p31_qids = get_claim_ids(claims, "P31")
331
- # entity_type = resolve_type_from_wikidata(p31_qids) if p31_qids else "UNK"
332
-
333
- # # useful extra claims
334
- # occupation_qids = get_claim_ids(claims, "P106")
335
- # country_qids = get_claim_ids(claims, "P17")
336
- # instance_of_qids = get_claim_ids(claims, "P31")
337
-
338
- # date_of_birth = next(iter(get_claim_strings(claims, "P569")), None)
339
- # date_of_death = next(iter(get_claim_strings(claims, "P570")), None)
340
- # coordinates = get_claim_coordinates(claims, "P625")
341
-
342
- # label_qids = list(set(occupation_qids + country_qids + instance_of_qids))
343
- # resolved_labels = get_entity_labels(label_qids, language=language)
344
-
345
- # occupations = [resolved_labels.get(q, q) for q in occupation_qids]
346
- # country = [resolved_labels.get(q, q) for q in country_qids]
347
- # instance_of = [resolved_labels.get(q, q) for q in instance_of_qids]
348
-
349
- # # ---- Wikipedia summary ----
350
- # wikipedia_summary = None
351
- # if wikipedia_title != "NIL":
352
- # wp_summary_url = f"https://{language}.wikipedia.org/api/rest_v1/page/summary/{wikipedia_title}"
353
- # wp_resp = safe_get(wp_summary_url, params={})
354
- # if wp_resp and wp_resp.status_code == 200:
355
- # try:
356
- # wp_data = wp_resp.json()
357
- # wikipedia_summary = wp_data.get("extract")
358
- # except Exception:
359
- # wikipedia_summary = None
360
-
361
- # return {
362
- # "wikidata_label": wikidata_label,
363
- # "wikidata_description": wikidata_description,
364
- # "wikidata_aliases": wikidata_aliases,
365
- # "wikidata_summary": wikidata_description, # practical summary proxy
366
- # "wikipedia_title": wikipedia_title,
367
- # "wikipedia_url": wikipedia_url,
368
- # "wikipedia_summary": wikipedia_summary,
369
- # "entity_type": entity_type,
370
- # "instance_of": instance_of,
371
- # "occupations": occupations,
372
- # "country": country,
373
- # "date_of_birth": date_of_birth,
374
- # "date_of_death": date_of_death,
375
- # "coordinates": coordinates,
376
- # }
377
-
378
- # except Exception as e:
379
- # print(f"Error parsing details for {qid}: {e}")
380
- # return {
381
- # "wikidata_label": qid,
382
- # "wikidata_description": None,
383
- # "wikidata_aliases": [],
384
- # "wikidata_summary": None,
385
- # "wikipedia_title": "NIL",
386
- # "wikipedia_url": "None",
387
- # "wikipedia_summary": None,
388
- # "entity_type": "UNK",
389
- # "instance_of": [],
390
- # "occupations": [],
391
- # "country": [],
392
- # "date_of_birth": None,
393
- # "date_of_death": None,
394
- # "coordinates": [],
395
- # }
396
-
397
-
398
- # GLOBALISE
399
  def get_wikidata_details(qid, language="en"):
400
  """
401
- Wikidata/Wikipedia enrichment tailored to GLOBALISE-style commodity / materia medica entities.
 
402
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  if not qid or qid == "NIL":
404
- return {
405
- "wikidata_label": "NIL",
406
- "wikidata_description": None,
407
- "wikidata_aliases": [],
408
- "wikipedia_title": "NIL",
409
- "wikipedia_url": "None",
410
- "wikipedia_summary": None,
411
- "entity_type": "other",
412
- "instance_of": [],
413
- "subclass_of": [],
414
- "scientific_name": [],
415
- "source_organism": [],
416
- "parent_taxon": [],
417
- "taxon_rank": [],
418
- "part_used": [],
419
- "derived_from": "unknown",
420
- "native_to": [],
421
- "country_of_origin": [],
422
- "use_categories": [],
423
- "medicinal_uses": [],
424
- "has_effect": [],
425
- }
426
 
427
  wd_url = "https://www.wikidata.org/w/api.php"
428
  wd_params = {
@@ -436,32 +288,11 @@ def get_wikidata_details(qid, language="en"):
436
 
437
  resp = safe_get(wd_url, wd_params)
438
  if not resp:
439
- return {
440
- "wikidata_label": qid,
441
- "wikidata_description": None,
442
- "wikidata_aliases": [],
443
- "wikipedia_title": "NIL",
444
- "wikipedia_url": "None",
445
- "wikipedia_summary": None,
446
- "entity_type": "other",
447
- "instance_of": [],
448
- "subclass_of": [],
449
- "scientific_name": [],
450
- "source_organism": [],
451
- "parent_taxon": [],
452
- "taxon_rank": [],
453
- "part_used": [],
454
- "derived_from": "unknown",
455
- "native_to": [],
456
- "country_of_origin": [],
457
- "use_categories": [],
458
- "medicinal_uses": [],
459
- "has_effect": [],
460
- }
461
 
462
  try:
463
  data = resp.json()
464
- entity = data["entities"][qid]
465
 
466
  labels = entity.get("labels", {})
467
  descriptions = entity.get("descriptions", {})
@@ -475,10 +306,9 @@ def get_wikidata_details(qid, language="en"):
475
  or qid
476
  )
477
 
478
- wikidata_description = (
479
- descriptions.get(language, {}).get("value")
480
- or descriptions.get("en", {}).get("value")
481
- )
482
 
483
  wikidata_aliases = [
484
  a["value"] for a in (aliases.get(language) or aliases.get("en") or [])
@@ -503,60 +333,21 @@ def get_wikidata_details(qid, language="en"):
503
 
504
  instance_of_qids = get_claim_entity_ids(claims, "P31")
505
  subclass_of_qids = get_claim_entity_ids(claims, "P279")
506
- parent_taxon_qids = get_claim_entity_ids(claims, "P171")
507
- taxon_rank_qids = get_claim_entity_ids(claims, "P105")
508
- country_of_origin_qids = get_claim_entity_ids(claims, "P495")
509
- use_qids = get_claim_entity_ids(claims, "P366")
510
- medicinal_use_qids = get_claim_entity_ids(claims, "P2176")
511
-
512
- scientific_name = get_claim_string_values(claims, "P225")
513
-
514
- label_qids = list(set(
515
- instance_of_qids
516
- + subclass_of_qids
517
- + parent_taxon_qids
518
- + taxon_rank_qids
519
- + country_of_origin_qids
520
- + use_qids
521
- + medicinal_use_qids
522
- ))
523
  label_map = get_entity_labels(label_qids, language=language)
524
 
525
  instance_of = [label_map.get(q, q) for q in instance_of_qids]
526
  subclass_of = [label_map.get(q, q) for q in subclass_of_qids]
527
- parent_taxon = [label_map.get(q, q) for q in parent_taxon_qids]
528
- taxon_rank = [label_map.get(q, q) for q in taxon_rank_qids]
529
- country_of_origin = [label_map.get(q, q) for q in country_of_origin_qids]
530
- medicinal_uses = [label_map.get(q, q) for q in medicinal_use_qids]
531
-
532
- combined_text = " ".join(filter(None, [
533
- wikidata_label,
534
- wikidata_description,
535
- wikipedia_summary,
536
- " ".join(instance_of),
537
- " ".join(subclass_of),
538
- ]))
539
-
540
- use_categories = infer_use_categories(combined_text)
541
- derived_from = infer_origin_type(instance_of, subclass_of, wikidata_description, wikipedia_summary)
542
-
543
- entity_type = resolve_globalise_type(
544
- instance_of_labels=instance_of,
545
- subclass_of_labels=subclass_of,
546
- description=wikidata_description
547
- )
548
 
549
- # weak heuristic fields derived from text
550
- part_used = []
551
- text_lower = combined_text.lower()
552
- for part in ["root", "seed", "fruit", "flower", "pod", "bark", "wood", "resin", "gum", "oil", "leaf", "bulb"]:
553
- if part in text_lower:
554
- part_used.append(part)
555
-
556
- has_effect = []
557
- for effect in ["laxative", "styptic", "febrifuge", "aromatic", "fragrant", "bitter", "blood-staunching"]:
558
- if effect in text_lower:
559
- has_effect.append(effect)
560
 
561
  return {
562
  "wikidata_label": wikidata_label,
@@ -565,117 +356,21 @@ def get_wikidata_details(qid, language="en"):
565
  "wikipedia_title": wikipedia_title,
566
  "wikipedia_url": wikipedia_url,
567
  "wikipedia_summary": wikipedia_summary,
568
-
569
  "entity_type": entity_type,
570
  "instance_of": instance_of,
571
  "subclass_of": subclass_of,
572
-
573
- "scientific_name": scientific_name,
574
- "source_organism": parent_taxon, # pragmatic reuse
575
- "parent_taxon": parent_taxon,
576
- "taxon_rank": taxon_rank,
577
-
578
- "part_used": sorted(set(part_used)),
579
- "derived_from": derived_from,
580
- "native_to": [],
581
- "country_of_origin": country_of_origin,
582
-
583
- "use_categories": use_categories,
584
- "medicinal_uses": medicinal_uses,
585
- "has_effect": sorted(set(has_effect)),
586
  }
587
 
588
  except Exception as e:
589
- print(f"Error parsing details for {qid}: {e}")
590
- return {
591
- "wikidata_label": qid,
592
- "wikidata_description": None,
593
- "wikidata_aliases": [],
594
- "wikipedia_title": "NIL",
595
- "wikipedia_url": "None",
596
- "wikipedia_summary": None,
597
- "entity_type": "other",
598
- "instance_of": [],
599
- "subclass_of": [],
600
- "scientific_name": [],
601
- "source_organism": [],
602
- "parent_taxon": [],
603
- "taxon_rank": [],
604
- "part_used": [],
605
- "derived_from": "unknown",
606
- "native_to": [],
607
- "country_of_origin": [],
608
- "use_categories": [],
609
- "medicinal_uses": [],
610
- "has_effect": [],
611
- }
612
-
613
-
614
-
615
- def infer_use_categories(text):
616
-
617
- """
618
-
619
- Infer useful GLOBALISE-style categories from Wikipedia/Wikidata text.
620
-
621
- """
622
-
623
- text = (text or "").lower()
624
-
625
- cats = []
626
-
627
- if any(x in text for x in ["medicine", "medicinal", "medicament", "drug", "pharmacopoeia", "laxative", "styptic", "febrifuge", "remedy"]):
628
-
629
- cats.append("medicine")
630
-
631
- if any(x in text for x in ["dye", "pigment", "color", "colour", "colorant"]):
632
-
633
- cats.append("dye")
634
-
635
- if any(x in text for x in ["spice", "seasoning", "aromatic", "cardamom", "pepper", "cubeb"]):
636
-
637
- cats.append("spice")
638
-
639
- if any(x in text for x in ["perfume", "fragrant", "incense", "odoriferous", "aromatic resin"]):
640
-
641
- cats.append("perfume_incense")
642
-
643
- if any(x in text for x in ["food", "edible", "eaten", "culinary", "delicacy", "sweet", "used as food"]):
644
-
645
- cats.append("food")
646
-
647
- if any(x in text for x in ["trade", "commodity", "commerce", "traded", "export"]):
648
 
649
- cats.append("trade_good")
650
 
651
- return sorted(set(cats))
652
-
653
- def infer_origin_type(instance_of_labels, subclass_of_labels, description, wikipedia_summary):
654
-
655
- """
656
-
657
- Infer whether the entity is mainly plant-derived, animal-derived, mineral, or mixed/unknown.
658
-
659
- """
660
-
661
- text = " ".join((instance_of_labels or []) + (subclass_of_labels or [])) + " " + (description or "") + " " + (wikipedia_summary or "")
662
-
663
- text = text.lower()
664
-
665
- if any(x in text for x in ["plant", "tree", "shrub", "herb", "seed", "root", "fruit", "flower", "resin from", "gum from", "taxon", "species"]):
666
-
667
- return "plant"
668
-
669
- if any(x in text for x in ["animal", "whale", "pig", "secretion", "gall", "insect"]):
670
-
671
- return "animal"
672
-
673
- if any(x in text for x in ["mineral", "earth", "clay", "ore", "stone"]):
674
-
675
- return "mineral"
676
-
677
- return "unknown"
678
-
679
  def get_entity_labels(qids, language="en"):
680
  if not qids:
681
  return {}
@@ -706,7 +401,8 @@ def get_entity_labels(qids, language="en"):
706
  return out
707
  except Exception:
708
  return {}
709
-
 
710
  def get_wikipedia_page_props(input_str: str):
711
  """
712
  Retrieve the Wikidata QID for a given Wikipedia page name.
@@ -819,9 +515,17 @@ def resolve_type_from_wikidata(p31_qids, visited=None, depth=0, max_depth=3):
819
  # direct anchors
820
  if q == "Q5":
821
  return "pers"
822
- if q in qid_to_organization_label_keys or q in TYPE_MAP and TYPE_MAP[q] == "organization":
 
 
 
 
823
  return "org"
824
- if q in qid_to_location_label_keys or q in TYPE_MAP and TYPE_MAP[q] == "location":
 
 
 
 
825
  return "loc"
826
 
827
  # subclass traversal
@@ -844,7 +548,9 @@ def resolve_type_from_wikidata(p31_qids, visited=None, depth=0, max_depth=3):
844
  for c in claims["P279"]
845
  if "datavalue" in c["mainsnak"]
846
  ]
847
- resolved = resolve_type_from_wikidata(parents, visited, depth + 1, max_depth)
 
 
848
  if resolved != "UNK":
849
  return resolved
850
 
@@ -900,47 +606,7 @@ def get_wikipedia_title(qid, language="en"):
900
  print(f"Error parsing Wikidata for {qid}: {e}")
901
  return "NIL", "None", "UNK"
902
 
903
- def resolve_globalise_type(instance_of_labels, subclass_of_labels, description=None):
904
- """
905
- Map Wikidata labels/descriptions into a domain-specific coarse type
906
- suitable for GLOBALISE glossary entities.
907
- """
908
- labels = " | ".join((instance_of_labels or []) + (subclass_of_labels or [])).lower()
909
- desc = (description or "").lower()
910
- text = labels + " " + desc
911
-
912
- if any(x in text for x in ["plant", "taxon", "species", "genus", "tree", "shrub", "herb"]):
913
- return "plant"
914
-
915
- if any(x in text for x in ["spice", "condiment", "seed", "pepper", "cardamom", "cubeb"]):
916
- return "spice"
917
-
918
- if any(x in text for x in ["resin", "gum", "gum resin", "oleoresin", "benzoin", "myrrh", "lac"]):
919
- return "resin_gum"
920
-
921
- if any(x in text for x in ["oil", "essential oil"]):
922
- return "oil_extract"
923
-
924
- if any(x in text for x in ["extract", "compound", "chemical substance", "preparation"]):
925
- return "extract_preparation"
926
 
927
- if any(x in text for x in ["medicine", "drug", "materia medica", "medicament", "pharmaceutical"]):
928
- return "medicinal_substance"
929
-
930
- if any(x in text for x in ["dye", "pigment", "colorant"]):
931
- return "dye_colorant"
932
-
933
- if any(x in text for x in ["food", "fruit", "root vegetable", "yam", "edible plant", "crop"]):
934
- return "foodstuff"
935
-
936
- if any(x in text for x in ["animal product", "secretion", "ambergris", "bezoar", "gallstone"]):
937
- return "animal_derived_substance"
938
-
939
- if any(x in text for x in ["commodity", "trade good", "article of commerce", "material"]):
940
- return "commodity"
941
-
942
- return "other"
943
-
944
  class NelPipeline(Pipeline):
945
  """
946
  Hugging Face Pipeline for NEL that supports:
@@ -981,12 +647,9 @@ class NelPipeline(Pipeline):
981
  else:
982
  enclosed, lOffset, rOffset = None, None, None
983
 
984
- batch.append({
985
- "text": t,
986
- "surface": enclosed,
987
- "lOffset": lOffset,
988
- "rOffset": rOffset
989
- })
990
 
991
  return batch
992
 
@@ -998,18 +661,14 @@ class NelPipeline(Pipeline):
998
  and keeps the first 3 predictions per input.
999
  """
1000
  texts = [b["text"] for b in batch]
1001
-
1002
  enc = self.tokenizer(
1003
- texts,
1004
- return_tensors="pt",
1005
- padding=True,
1006
- truncation=True,
1007
- max_length=30
1008
  ).to(self.device)
1009
-
1010
  num_beams = 5
1011
  num_return_sequences = 5
1012
-
1013
  outputs = self.model.generate(
1014
  **enc,
1015
  num_beams=num_beams,
@@ -1017,152 +676,88 @@ class NelPipeline(Pipeline):
1017
  return_dict_in_generate=True,
1018
  output_scores=True,
1019
  )
1020
-
1021
  decoded = self.tokenizer.batch_decode(
1022
- outputs.sequences,
1023
- skip_special_tokens=True
1024
  )
1025
-
1026
  sequence_scores = outputs["sequences_scores"].cpu().numpy()
1027
  confidences = np.exp(sequence_scores)
1028
-
1029
  top_k = 3
1030
-
1031
  # flattened:
1032
  # [input1_seq1, ..., input1_seq5, input2_seq1, ..., inputN_seq5]
1033
  for i, item in enumerate(batch):
1034
  start = i * num_return_sequences
1035
  end = start + num_return_sequences
1036
-
1037
  item_predictions = decoded[start:end]
1038
  item_confidences = confidences[start:end]
1039
-
1040
  item["top_predictions"] = item_predictions[:top_k]
1041
  item["top_confidences"] = [float(x) for x in item_confidences[:top_k]]
1042
-
1043
  # optional debug: keep all 5
1044
  item["all_predictions"] = item_predictions
1045
  item["all_confidences"] = [float(x) for x in item_confidences]
1046
-
1047
  return batch
1048
 
1049
  def postprocess(self, batch, **kwargs):
1050
  """
1051
- N items -> N linked entities, each with top 3 enriched predictions
1052
- adapted to GLOBALISE commodity / medicinal / botanical entities.
1053
  """
1054
  results = []
1055
-
1056
  for item in batch:
1057
  surface = item["surface"]
1058
  lOffset = item.get("lOffset")
1059
  rOffset = item.get("rOffset")
1060
-
1061
  resolved_predictions = []
1062
-
1063
  for pred, conf in zip(item["top_predictions"], item["top_confidences"]):
1064
  qid, lang = get_wikipedia_page_props(pred)
1065
  details = get_wikidata_details(qid, language=lang)
1066
-
1067
  if conf > 1:
1068
  conf = conf / 100
1069
- conf = round(conf, 4)
1070
-
1071
- resolved_predictions.append({
1072
- "wkd_id": qid,
1073
- "wkd_pred": pred,
1074
-
1075
- "wkpedia_pagename": details["wikipedia_title"],
1076
- "wkpedia_url": details["wikipedia_url"],
1077
- "wikipedia_summary": details["wikipedia_summary"],
1078
-
1079
- "wikidata_label": details["wikidata_label"],
1080
- "wikidata_description": details["wikidata_description"],
1081
- "wikidata_aliases": details["wikidata_aliases"],
1082
-
1083
- "entity_type": details["entity_type"],
1084
- "instance_of": details["instance_of"],
1085
- "subclass_of": details["subclass_of"],
1086
-
1087
- "scientific_name": details["scientific_name"],
1088
- "source_organism": details["source_organism"],
1089
- "parent_taxon": details["parent_taxon"],
1090
- "taxon_rank": details["taxon_rank"],
1091
-
1092
- "part_used": details["part_used"],
1093
- "derived_from": details["derived_from"],
1094
- "native_to": details["native_to"],
1095
- "country_of_origin": details["country_of_origin"],
1096
-
1097
- "use_categories": details["use_categories"],
1098
- "medicinal_uses": details["medicinal_uses"],
1099
- "has_effect": details["has_effect"],
1100
-
1101
- "confidence_nel": conf,
1102
- })
1103
-
1104
- results.append({
1105
- "surface": surface,
1106
- "lOffset": lOffset,
1107
- "rOffset": rOffset,
1108
- "predictions": resolved_predictions,
1109
- })
1110
-
1111
  return results
1112
-
1113
- # def postprocess(self, batch, **kwargs):
1114
- # """
1115
- # N items -> N linked entities, each with top 3 enriched predictions
1116
- # """
1117
- # results = []
1118
-
1119
- # for item in batch:
1120
- # surface = item["surface"]
1121
- # lOffset = item.get("lOffset")
1122
- # rOffset = item.get("rOffset")
1123
-
1124
- # resolved_predictions = []
1125
-
1126
- # for pred, conf in zip(item["top_predictions"], item["top_confidences"]):
1127
- # qid, lang = get_wikipedia_page_props(pred)
1128
- # details = get_wikidata_details(qid, language=lang)
1129
-
1130
- # if conf > 1:
1131
- # conf = conf / 100
1132
- # conf = round(conf, 4)
1133
-
1134
- # resolved_predictions.append({
1135
- # "wkd_id": qid,
1136
- # "wkd_pred": pred,
1137
-
1138
- # "wkpedia_pagename": details["wikipedia_title"],
1139
- # "wkpedia_url": details["wikipedia_url"],
1140
- # "wikipedia_summary": details["wikipedia_summary"],
1141
-
1142
- # "wikidata_label": details["wikidata_label"],
1143
- # "wikidata_description": details["wikidata_description"],
1144
- # "wikidata_summary": details["wikidata_summary"],
1145
- # "wikidata_aliases": details["wikidata_aliases"],
1146
-
1147
- # "type": details["entity_type"],
1148
- # "instance_of": details["instance_of"],
1149
- # # "occupations": details["occupations"],
1150
- # "country": details["country"],
1151
- # # "date_of_birth": details["date_of_birth"],
1152
- # # "date_of_death": details["date_of_death"],
1153
- # "coordinates": details["coordinates"],
1154
-
1155
- # "confidence_nel": conf,
1156
- # })
1157
-
1158
- # results.append({
1159
- # "surface": surface,
1160
- # "lOffset": lOffset,
1161
- # "rOffset": rOffset,
1162
- # "predictions": resolved_predictions,
1163
- # })
1164
-
1165
- # return results
1166
 
1167
 
1168
  qid_to_location_label = {
 
1
  from transformers import Pipeline
2
  import requests
 
3
  import time
4
  import os
5
  import numpy as np
 
15
 
16
  NEL_MODEL = "nel-mgenre-multilingual"
17
 
18
+ HEADERS = {"User-Agent": "NEL-HF-Space/0.1 (contact: emanuela.boros@univ-lr.fr)"}
 
 
19
 
20
  WIKIMEDIA_QPS = float(os.getenv("WIKIMEDIA_QPS", "3.0")) # total across ALL processes
21
  _MIN_INTERVAL = 1.0 / max(WIKIMEDIA_QPS, 0.1)
22
 
23
  _LOCK = FileLock(os.getenv("WIKIMEDIA_RATELOCK", "/tmp/wikimedia_rate.lock"))
24
  _TS_FILE = os.getenv("WIKIMEDIA_RATETS", "/tmp/wikimedia_rate.ts")
25
+
26
  requests_cache.install_cache(
27
  cache_name=os.getenv("WIKIMEDIA_CACHE", "wikimedia_cache"),
28
  backend="sqlite",
 
41
  backoff_factor=float(os.getenv("WIKIMEDIA_BACKOFF", "1.5")),
42
  status_forcelist=(429, 500, 502, 503, 504),
43
  allowed_methods=frozenset(["GET"]),
44
+ respect_retry_after_header=False, # <-- IMPORTANT
45
  raise_on_status=False,
46
  )
47
 
 
49
  _session.mount("https://", _adapter)
50
  _session.mount("http://", _adapter)
51
 
52
+
53
  def safe_get(url, params, retries=3, backoff=2):
54
  for attempt in range(retries):
55
  try:
 
57
  if resp.status_code == 200:
58
  return resp
59
  elif resp.status_code in (403, 429): # forbidden or rate limited
60
+ wait = backoff**attempt
61
  print(f"Retrying {url} in {wait}s (status={resp.status_code})")
62
  time.sleep(wait)
63
  continue
 
65
  resp.raise_for_status()
66
  except Exception as e:
67
  print(f"Request error: {e}")
68
+ time.sleep(backoff**attempt)
69
  return None
70
 
71
 
 
104
  values.append(v["time"])
105
  return values
106
 
107
+
108
  def extract_datavalue(claim):
109
  try:
110
  return claim["mainsnak"]["datavalue"]["value"]
 
134
  vals.append(v["time"])
135
  return vals
136
 
137
+
138
  def get_claim_coordinates(claims, prop="P625"):
139
  """
140
  Return coordinates from Wikidata if present.
 
143
  for c in claims.get(prop, []):
144
  v = extract_datavalue(c)
145
  if isinstance(v, dict) and "latitude" in v and "longitude" in v:
146
+ coords.append(
147
+ {
148
+ "latitude": v["latitude"],
149
+ "longitude": v["longitude"],
150
+ "precision": v.get("precision"),
151
+ }
152
+ )
153
  return coords
154
 
155
 
 
229
  # Even with Retry-After handling, sometimes you still get here.
230
  ra = resp.headers.get("Retry-After")
231
  sleep = float(ra) if (ra and ra.isdigit()) else (1.0 + random.random())
232
+ logger.warning(
233
+ f"[WIKI] 429 rate limited. retry-after={ra} sleeping={sleep:.2f}s url={url}"
234
+ )
235
  time.sleep(sleep)
236
  return resp # caller may treat as failure; Retry already happened.
237
 
238
+ logger.warning(
239
+ f"[WIKI] HTTP {resp.status_code} url={url} params={_short(params)}"
240
+ )
241
  return resp
242
  except Exception as e:
243
  logger.exception(f"[WIKI] Request error: {e} url={url}")
 
248
  s = str(params)
249
  return s if len(s) <= maxlen else s[:maxlen] + "..."
250
 
251
+
252
+ # Generic Wikidata/Wikipedia enrichment
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  def get_wikidata_details(qid, language="en"):
254
  """
255
+ Generic Wikidata/Wikipedia enrichment for historical named entity linking.
256
+ Works for persons, locations, organizations, and other entities.
257
  """
258
+ empty = {
259
+ "wikidata_label": qid or "NIL",
260
+ "wikidata_description": None,
261
+ "wikidata_aliases": [],
262
+ "wikipedia_title": "NIL",
263
+ "wikipedia_url": "None",
264
+ "wikipedia_summary": None,
265
+ "entity_type": "UNK",
266
+ "instance_of": [],
267
+ "subclass_of": [],
268
+ "date_of_birth": [],
269
+ "date_of_death": [],
270
+ "coordinates": [],
271
+ "country": [],
272
+ "occupation": [],
273
+ }
274
+
275
  if not qid or qid == "NIL":
276
+ empty["wikidata_label"] = "NIL"
277
+ return empty
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
  wd_url = "https://www.wikidata.org/w/api.php"
280
  wd_params = {
 
288
 
289
  resp = safe_get(wd_url, wd_params)
290
  if not resp:
291
+ return empty
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
  try:
294
  data = resp.json()
295
+ entity = data.get("entities", {}).get(qid, {})
296
 
297
  labels = entity.get("labels", {})
298
  descriptions = entity.get("descriptions", {})
 
306
  or qid
307
  )
308
 
309
+ wikidata_description = descriptions.get(language, {}).get(
310
+ "value"
311
+ ) or descriptions.get("en", {}).get("value")
 
312
 
313
  wikidata_aliases = [
314
  a["value"] for a in (aliases.get(language) or aliases.get("en") or [])
 
333
 
334
  instance_of_qids = get_claim_entity_ids(claims, "P31")
335
  subclass_of_qids = get_claim_entity_ids(claims, "P279")
336
+ occupation_qids = get_claim_entity_ids(claims, "P106")
337
+ country_qids = get_claim_entity_ids(claims, "P17")
338
+
339
+ label_qids = list(
340
+ set(instance_of_qids + subclass_of_qids + occupation_qids + country_qids)
341
+ )
342
+
 
 
 
 
 
 
 
 
 
 
343
  label_map = get_entity_labels(label_qids, language=language)
344
 
345
  instance_of = [label_map.get(q, q) for q in instance_of_qids]
346
  subclass_of = [label_map.get(q, q) for q in subclass_of_qids]
347
+ occupation = [label_map.get(q, q) for q in occupation_qids]
348
+ country = [label_map.get(q, q) for q in country_qids]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
+ entity_type = resolve_type_from_wikidata(instance_of_qids)
 
 
 
 
 
 
 
 
 
 
351
 
352
  return {
353
  "wikidata_label": wikidata_label,
 
356
  "wikipedia_title": wikipedia_title,
357
  "wikipedia_url": wikipedia_url,
358
  "wikipedia_summary": wikipedia_summary,
 
359
  "entity_type": entity_type,
360
  "instance_of": instance_of,
361
  "subclass_of": subclass_of,
362
+ "date_of_birth": get_claim_string_values(claims, "P569"),
363
+ "date_of_death": get_claim_string_values(claims, "P570"),
364
+ "coordinates": get_claim_coordinates(claims, "P625"),
365
+ "country": country,
366
+ "occupation": occupation,
 
 
 
 
 
 
 
 
 
367
  }
368
 
369
  except Exception as e:
370
+ logger.exception(f"Error parsing Wikidata details for {qid}: {e}")
371
+ return empty
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
 
 
373
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  def get_entity_labels(qids, language="en"):
375
  if not qids:
376
  return {}
 
401
  return out
402
  except Exception:
403
  return {}
404
+
405
+
406
  def get_wikipedia_page_props(input_str: str):
407
  """
408
  Retrieve the Wikidata QID for a given Wikipedia page name.
 
515
  # direct anchors
516
  if q == "Q5":
517
  return "pers"
518
+ if (
519
+ q in qid_to_organization_label_keys
520
+ or q in TYPE_MAP
521
+ and TYPE_MAP[q] == "organization"
522
+ ):
523
  return "org"
524
+ if (
525
+ q in qid_to_location_label_keys
526
+ or q in TYPE_MAP
527
+ and TYPE_MAP[q] == "location"
528
+ ):
529
  return "loc"
530
 
531
  # subclass traversal
 
548
  for c in claims["P279"]
549
  if "datavalue" in c["mainsnak"]
550
  ]
551
+ resolved = resolve_type_from_wikidata(
552
+ parents, visited, depth + 1, max_depth
553
+ )
554
  if resolved != "UNK":
555
  return resolved
556
 
 
606
  print(f"Error parsing Wikidata for {qid}: {e}")
607
  return "NIL", "None", "UNK"
608
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
610
  class NelPipeline(Pipeline):
611
  """
612
  Hugging Face Pipeline for NEL that supports:
 
647
  else:
648
  enclosed, lOffset, rOffset = None, None, None
649
 
650
+ batch.append(
651
+ {"text": t, "surface": enclosed, "lOffset": lOffset, "rOffset": rOffset}
652
+ )
 
 
 
653
 
654
  return batch
655
 
 
661
  and keeps the first 3 predictions per input.
662
  """
663
  texts = [b["text"] for b in batch]
664
+
665
  enc = self.tokenizer(
666
+ texts, return_tensors="pt", padding=True, truncation=True, max_length=30
 
 
 
 
667
  ).to(self.device)
668
+
669
  num_beams = 5
670
  num_return_sequences = 5
671
+
672
  outputs = self.model.generate(
673
  **enc,
674
  num_beams=num_beams,
 
676
  return_dict_in_generate=True,
677
  output_scores=True,
678
  )
679
+
680
  decoded = self.tokenizer.batch_decode(
681
+ outputs.sequences, skip_special_tokens=True
 
682
  )
683
+
684
  sequence_scores = outputs["sequences_scores"].cpu().numpy()
685
  confidences = np.exp(sequence_scores)
686
+
687
  top_k = 3
688
+
689
  # flattened:
690
  # [input1_seq1, ..., input1_seq5, input2_seq1, ..., inputN_seq5]
691
  for i, item in enumerate(batch):
692
  start = i * num_return_sequences
693
  end = start + num_return_sequences
694
+
695
  item_predictions = decoded[start:end]
696
  item_confidences = confidences[start:end]
697
+
698
  item["top_predictions"] = item_predictions[:top_k]
699
  item["top_confidences"] = [float(x) for x in item_confidences[:top_k]]
700
+
701
  # optional debug: keep all 5
702
  item["all_predictions"] = item_predictions
703
  item["all_confidences"] = [float(x) for x in item_confidences]
704
+
705
  return batch
706
 
707
  def postprocess(self, batch, **kwargs):
708
  """
709
+ N items -> N linked entities, each with top-k enriched Wikidata/Wikipedia predictions.
710
+ Generic version for historical NEL.
711
  """
712
  results = []
713
+
714
  for item in batch:
715
  surface = item["surface"]
716
  lOffset = item.get("lOffset")
717
  rOffset = item.get("rOffset")
718
+
719
  resolved_predictions = []
720
+
721
  for pred, conf in zip(item["top_predictions"], item["top_confidences"]):
722
  qid, lang = get_wikipedia_page_props(pred)
723
  details = get_wikidata_details(qid, language=lang)
724
+
725
  if conf > 1:
726
  conf = conf / 100
727
+ conf = round(float(conf), 4)
728
+
729
+ resolved_predictions.append(
730
+ {
731
+ "wkd_id": qid,
732
+ "wkd_pred": pred,
733
+ "wkpedia_pagename": details["wikipedia_title"],
734
+ "wkpedia_url": details["wikipedia_url"],
735
+ "wikipedia_summary": details["wikipedia_summary"],
736
+ "wikidata_label": details["wikidata_label"],
737
+ "wikidata_description": details["wikidata_description"],
738
+ "wikidata_aliases": details["wikidata_aliases"],
739
+ "entity_type": details["entity_type"],
740
+ "instance_of": details["instance_of"],
741
+ "subclass_of": details["subclass_of"],
742
+ "date_of_birth": details["date_of_birth"],
743
+ "date_of_death": details["date_of_death"],
744
+ "coordinates": details["coordinates"],
745
+ "country": details["country"],
746
+ "occupation": details["occupation"],
747
+ "confidence_nel": conf,
748
+ }
749
+ )
750
+
751
+ results.append(
752
+ {
753
+ "surface": surface,
754
+ "lOffset": lOffset,
755
+ "rOffset": rOffset,
756
+ "predictions": resolved_predictions,
757
+ }
758
+ )
759
+
 
 
 
 
 
 
 
 
 
760
  return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
761
 
762
 
763
  qid_to_location_label = {