From e630dc8071808893b44bb8dbe5da8f95517118a3 Mon Sep 17 00:00:00 2001
From: Rick Lupton <mail@ricklupton.name>
Date: Sun, 19 Nov 2023 14:52:05 +0000
Subject: [PATCH 2/2] org-id.el: Extend links with search strings, inherit
 parent IDs

* lisp/ol.el (org-store-link): Refactor org-id links to use standard
`org-store-link-functions'.
(org-link-search): Create new headings at appropriate level.
(org-link-precise-link-target): New function extracting logic to
identify a precise link target, e.g. a heading, named object, or text
search.
(org-link-try-link-store-functions): Extract logic to call external
link store functions. Pass them a new `interactive?' argument.
* lisp/ol-bbdb.el (org-bbdb-store-link):
* lisp/ol-bibtex.el (org-bibtex-store-link):
* lisp/ol-docview.el (org-docview-store-link):
* lisp/ol-eshell.el (org-eshell-store-link):
* lisp/ol-eww.el (org-eww-store-link):
* lisp/ol-gnus.el (org-gnus-store-link):
* lisp/ol-info.el (org-info-store-link):
* lisp/ol-irc.el (org-irc-store-link):
* lisp/ol-man.el (org-man-store-link):
* lisp/ol-mhe.el (org-mhe-store-link):
* lisp/ol-rmail.el (org-rmail-store-link): Accept optional arg.
* lisp/org-id.el (org-id-link-consider-parent-id): New option to allow
a parent heading with an id to be considered as a link target.
(org-id-link-use-context): New option to add context to org-id links.
(org-id-get): Add optional `inherit' argument which considers parents'
IDs if the current entry does not have one.
(org-id-store-link): Consider IDs of parent headings as link targets
when current heading has no ID and `org-id-link-consider-parent-id' is
set. Add a search string to the link when enabled.
(org-id-store-link-maybe): Function set as :store option for custom id
link property. Move logic from `org-store-link' here to determine when
an org-id link should be stored using `org-id-store-link'.
(org-id-open): Recognise search strings after "::" in org-id links.
* lisp/org-lint.el: add checker for "::" in ID properties.
* testing/lisp/test-ol.el: Add tests for
`org-link-precise-link-target' and `org-id-store-link' functions,
testing new options.
* doc/org-manual.org: Update documentation about links.
* etc/ORG-NEWS: Document changes and new options.

These feature allows for more precise links when using org-id to link to
org headings, without requiring every single headline to have an id.

Link: https://list.orgmode.org/118435e8-0b20-46fd-af6a-88de8e19fac6@app.fastmail.com/
---
 doc/org-manual.org      | 133 ++++++++++-------
 etc/ORG-NEWS            |  64 +++++++++
 lisp/ol-bbdb.el         |   2 +-
 lisp/ol-bibtex.el       |   2 +-
 lisp/ol-docview.el      |   2 +-
 lisp/ol-eshell.el       |   2 +-
 lisp/ol-eww.el          |   2 +-
 lisp/ol-gnus.el         |   2 +-
 lisp/ol-info.el         |   2 +-
 lisp/ol-irc.el          |   2 +-
 lisp/ol-man.el          |   2 +-
 lisp/ol-mhe.el          |   2 +-
 lisp/ol-rmail.el        |   2 +-
 lisp/ol.el              | 312 +++++++++++++++++++++++++---------------
 lisp/org-id.el          | 178 ++++++++++++++++++++---
 lisp/org-lint.el        |  16 +++
 testing/lisp/test-ol.el | 130 +++++++++++++++++
 17 files changed, 658 insertions(+), 197 deletions(-)

diff --git a/doc/org-manual.org b/doc/org-manual.org
index 64b4fd0f5..4596b401b 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -3297,10 +3297,6 @@ Here is the full set of built-in link types:
 
   File links.  File name may be remote, absolute, or relative.
 
-  Additionally, you can specify a line number, or a text search.
-  In Org files, you may link to a headline name, a custom ID, or a
-  code reference instead.
-
   As a special case, "file" prefix may be omitted if the file name
   is complete, e.g., it starts with =./=, or =/=.
 
@@ -3364,44 +3360,50 @@ Here is the full set of built-in link types:
 
   Execute a shell command upon activation.
 
+
+For =file:= and =id:= links, you can additionally specify a line
+number, or a text search string, separated by =::=.  In Org files, you
+may link to a headline name, a custom ID, or a code reference instead.
+
 The following table illustrates the link types above, along with their
 options:
 
-| Link Type  | Example                                                  |
-|------------+----------------------------------------------------------|
-| http       | =http://staff.science.uva.nl/c.dominik/=                 |
-| https      | =https://orgmode.org/=                                   |
-| doi        | =doi:10.1000/182=                                        |
-| file       | =file:/home/dominik/images/jupiter.jpg=                  |
-|            | =/home/dominik/images/jupiter.jpg= (same as above)       |
-|            | =file:papers/last.pdf=                                   |
-|            | =./papers/last.pdf= (same as above)                      |
-|            | =file:/ssh:me@some.where:papers/last.pdf= (remote)       |
-|            | =/ssh:me@some.where:papers/last.pdf= (same as above)     |
-|            | =file:sometextfile::NNN= (jump to line number)           |
-|            | =file:projects.org=                                      |
-|            | =file:projects.org::some words= (text search)[fn:12]     |
-|            | =file:projects.org::*task title= (headline search)       |
-|            | =file:projects.org::#custom-id= (headline search)        |
-| attachment | =attachment:projects.org=                                |
-|            | =attachment:projects.org::some words= (text search)      |
-| docview    | =docview:papers/last.pdf::NNN=                           |
-| id         | =id:B7423F4D-2E8A-471B-8810-C40F074717E9=                |
-| news       | =news:comp.emacs=                                        |
-| mailto     | =mailto:adent@galaxy.net=                                |
-| mhe        | =mhe:folder= (folder link)                               |
-|            | =mhe:folder#id= (message link)                           |
-| rmail      | =rmail:folder= (folder link)                             |
-|            | =rmail:folder#id= (message link)                         |
-| gnus       | =gnus:group= (group link)                                |
-|            | =gnus:group#id= (article link)                           |
-| bbdb       | =bbdb:R.*Stallman= (record with regexp)                  |
-| irc        | =irc:/irc.com/#emacs/bob=                                |
-| help       | =help:org-store-link=                                    |
-| info       | =info:org#External links=                                |
-| shell      | =shell:ls *.org=                                         |
-| elisp      | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) |
-|            | =elisp:org-agenda= (interactive Elisp command)           |
+| Link Type  | Example                                                            |
+|------------+--------------------------------------------------------------------|
+| http       | =http://staff.science.uva.nl/c.dominik/=                           |
+| https      | =https://orgmode.org/=                                             |
+| doi        | =doi:10.1000/182=                                                  |
+| file       | =file:/home/dominik/images/jupiter.jpg=                            |
+|            | =/home/dominik/images/jupiter.jpg= (same as above)                 |
+|            | =file:papers/last.pdf=                                             |
+|            | =./papers/last.pdf= (same as above)                                |
+|            | =file:/ssh:me@some.where:papers/last.pdf= (remote)                 |
+|            | =/ssh:me@some.where:papers/last.pdf= (same as above)               |
+|            | =file:sometextfile::NNN= (jump to line number)                     |
+|            | =file:projects.org=                                                |
+|            | =file:projects.org::some words= (text search)[fn:12]               |
+|            | =file:projects.org::*task title= (headline search)                 |
+|            | =file:projects.org::#custom-id= (headline search)                  |
+| attachment | =attachment:projects.org=                                          |
+|            | =attachment:projects.org::some words= (text search)                |
+| docview    | =docview:papers/last.pdf::NNN=                                     |
+| id         | =id:B7423F4D-2E8A-471B-8810-C40F074717E9=                          |
+|            | =id:B7423F4D-2E8A-471B-8810-C40F074717E9::*task= (headline search) |
+| news       | =news:comp.emacs=                                                  |
+| mailto     | =mailto:adent@galaxy.net=                                          |
+| mhe        | =mhe:folder= (folder link)                                         |
+|            | =mhe:folder#id= (message link)                                     |
+| rmail      | =rmail:folder= (folder link)                                       |
+|            | =rmail:folder#id= (message link)                                   |
+| gnus       | =gnus:group= (group link)                                          |
+|            | =gnus:group#id= (article link)                                     |
+| bbdb       | =bbdb:R.*Stallman= (record with regexp)                            |
+| irc        | =irc:/irc.com/#emacs/bob=                                          |
+| help       | =help:org-store-link=                                              |
+| info       | =info:org#External links=                                          |
+| shell      | =shell:ls *.org=                                                   |
+| elisp      | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate)           |
+|            | =elisp:org-agenda= (interactive Elisp command)                     |
 
 #+cindex: VM links
 #+cindex: Wanderlust links
@@ -3462,8 +3464,9 @@ current buffer:
 - /Org mode buffers/ ::
 
   For Org files, if there is a =<<target>>= at point, the link points
-  to the target.  Otherwise it points to the current headline, which
-  is also the description.
+  to the target.  If there is a named block (using =#+name:=) at
+  point, the link points to that name.  Otherwise it points to the
+  current headline, which is also the description.
 
   #+vindex: org-id-link-to-org-use-id
   #+cindex: @samp{CUSTOM_ID}, property
@@ -3481,6 +3484,30 @@ current buffer:
   timestamp, depending on ~org-id-method~.  Later, when inserting the
   link, you need to decide which one to use.
 
+  #+vindex: org-id-link-consider-parent-id
+  #+vindex: org-id-link-use-context
+  When ~org-id-link-consider-parent-id~ is ~t~ (and
+  ~org-link-context-for-files~ and ~org-id-link-use-context~ are both
+  enabled), parent =ID= properties are considered.  This allows
+  linking to specific targets, named blocks, or headlines (which may
+  not have a globally unique =ID= themselves) within the context of a
+  parent headline or file which does.
+
+  For example, given this org file with those variables set:
+
+  #+begin_src org
+  ,* Parent
+  :PROPERTIES:
+  :ID: abc
+  :END:
+  ,** Child 1
+  ,** Child 2
+  #+end_src
+
+  Storing a link with point at "Child 1" will produce a link
+  =<id:abc::*Child 1>=, which precisely links to the "Child 1"
+  headline even though it does not have its own ID.
+
 - /Email/News clients: VM, Rmail, Wanderlust, MH-E, Gnus/ ::
 
   #+vindex: org-link-email-description-format
@@ -3760,7 +3787,9 @@ the link completion function like this:
 :ALT_TITLE: Search Options
 :END:
 #+cindex: search option in file links
+#+cindex: search option in id links
 #+cindex: file links, searching
+#+cindex: id links, searching
 #+cindex: attachment links, searching
 
 File links can contain additional information to make Emacs jump to a
@@ -3772,8 +3801,8 @@ example, when the command ~org-store-link~ creates a link (see
 line as a search string that can be used to find this line back later
 when following the link with {{{kbd(C-c C-o)}}}.
 
-Note that all search options apply for Attachment links in the same
-way that they apply for File links.
+Note that all search options apply for Attachment and ID links in the
+same way that they apply for File links.
 
 Here is the syntax of the different ways to attach a search to a file
 link, together with explanations for each:
@@ -21360,7 +21389,7 @@ The following =ol-man.el= file implements it
 PATH should be a topic that can be thrown at the man command."
   (funcall org-man-command path))
 
-(defun org-man-store-link ()
+(defun org-man-store-link (&optional _interactive?)
   "Store a link to a man page."
   (when (memq major-mode '(Man-mode woman-mode))
     ;; This is a man page, we do make this link.
@@ -21420,13 +21449,15 @@ A review of =ol-man.el=:
 
    For example, ~org-man-store-link~ is responsible for storing a link
    when ~org-store-link~ (see [[*Handling Links]]) is called from a buffer
-   displaying a man page.  It first checks if the major mode is
-   appropriate.  If check fails, the function returns ~nil~, which
-   means it isn't responsible for creating a link to the current
-   buffer.  Otherwise the function makes a link string by combining
-   the =man:= prefix with the man topic.  It also provides a default
-   description.  The function ~org-insert-link~ can insert it back
-   into an Org buffer later on.
+   displaying a man page.  It is passed an argument ~interactive?~
+   which this function does not use, but other store functions use to
+   behave differently when a link is stored interactively by the user.
+   It first checks if the major mode is appropriate.  If check fails,
+   the function returns ~nil~, which means it isn't responsible for
+   creating a link to the current buffer.  Otherwise the function
+   makes a link string by combining the =man:= prefix with the man
+   topic.  It also provides a default description.  The function
+   ~org-insert-link~ can insert it back into an Org buffer later on.
 
 ** Adding Export Backends
 :PROPERTIES:
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index adda55532..6a3d49f6c 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -373,6 +373,14 @@ timestamp object.  Possible values: ~timerange~, ~daterange~, ~nil~.
 ~org-element-timestamp-interpreter~ takes into account this property
 and returns an appropriate timestamp string.
 
+**** =org-link= store functions are passed an ~interactive?~ argument
+
+The ~:store:~ functions set for link types using
+~org-link-set-parameters~ are now passed an ~interactive?~ argument,
+indicating whether ~org-store-link~ was called interactively.
+
+Existing store functions will continue to work.
+
 *** ~org-priority=show~ command no longer adjusts for scheduled/deadline
 
 In agenda views, ~org-priority=show~ command previously displayed the
@@ -451,6 +459,27 @@ The change is breaking when ~org-use-property-inheritance~ is set to ~t~.
 *** ~org-babel-lilypond-compile-lilyfile~ ignores optional second argument
 
 The =TEST= parameter is better served by Emacs debugging tools.
+
+*** ~org-id-store-link~ now adds search strings for precise link targets
+
+This new behaviour can be disabled generally by setting
+~org-id-link-use-context~ to ~nil~, or the setting can be toggled for
+a single call to ~org-store-link~ with a universal argument.
+
+When using this feature, IDs should not include =::=, which is used in
+links to indicate the start of the search string.  For backwards
+compability, existing IDs including =::= will still be matched (but
+cannot be used together with precise link targets).  An org-lint
+checker has been added to warn about this.
+
+*** ~org-store-link~ behaviour storing additional =CUSTOM_ID= links has changed
+
+As well as an =id:= link, ~org-store-link~ stores an additional "human
+readable" link using a node's =CUSTOM_ID= property, if available.
+This behaviour has been expanded to store an additional =CUSTOM_ID=
+link when storing any type of external link type in an Org file, not
+just =id:= links.
+
 ** New and changed options
 *** New custom setting ~org-icalendar-ttl~ for the ~ox-icalendar~ backend
 
@@ -709,6 +738,35 @@ This option starts the agenda to automatically include archives,
 propagating the value for this variable to ~org-agenda-archives-mode~.
 For acceptable values and their meaning, see the value of that variable.
 
+*** New option ~org-id-link-consider-parent-id~ to allow =id:= links to parent headlines
+
+For =id:= links, when this option is enabled, ~org-store-link~ will
+look for ids from parent/ancestor headlines, if the current headline
+does not have an id.
+
+Combined with the new ability for =id:= links to use search strings
+for precise link targets (when =org-id-link-use-context= is =t=, which
+is the default), this allows linking to specific headlines without
+requiring every headline to have an id property, as long as the
+headline is unique within a subtree that does have an id property.
+
+For example, given this org file:
+
+#+begin_src org
+,* Parent
+:PROPERTIES:
+:ID: abc
+:END:
+,** Child 1
+,** Child 2
+#+end_src
+
+Storing a link with point at "Child 1" will produce a link
+=<id:abc::*Child 1>=, which precisely links to the "Child 1" headline
+even though it does not have its own ID.  By giving files top-level id
+properties, links to headlines in the file can also be made more
+robust by using the file id instead of the file path.
+
 ** New features
 *** =ob-plantuml.el=: Support tikz file format output
 
@@ -997,6 +1055,12 @@ A numeric value forces a heading at that level to be inserted.  For
 backwards compatibility, non-numeric non-nil values insert level 1
 headings as before.
 
+*** New optional argument for ~org-id-get~
+
+New optional argument =INHERIT= means inherited ID properties from
+parent entries are considered when getting an entry's ID (see
+~org-id-link-consider-parent-id~ option).
+
 ** Miscellaneous
 *** =org-crypt.el= now applies initial visibility settings to decrypted entries
 
diff --git a/lisp/ol-bbdb.el b/lisp/ol-bbdb.el
index be3924fc9..6ea060f70 100644
--- a/lisp/ol-bbdb.el
+++ b/lisp/ol-bbdb.el
@@ -226,7 +226,7 @@ date year)."
 
 ;;; Implementation
 
-(defun org-bbdb-store-link ()
+(defun org-bbdb-store-link (&optional _interactive?)
   "Store a link to a BBDB database entry."
   (when (eq major-mode 'bbdb-mode)
     ;; This is BBDB, we make this link!
diff --git a/lisp/ol-bibtex.el b/lisp/ol-bibtex.el
index c5a950e2d..38468f32f 100644
--- a/lisp/ol-bibtex.el
+++ b/lisp/ol-bibtex.el
@@ -507,7 +507,7 @@ ARG, when non-nil, is a universal prefix argument.  See
 `org-open-file' for details."
   (org-link-open-as-file path arg))
 
-(defun org-bibtex-store-link ()
+(defun org-bibtex-store-link (&optional _interactive?)
   "Store a link to a BibTeX entry."
   (when (eq major-mode 'bibtex-mode)
     (let* ((search (org-create-file-search-in-bibtex))
diff --git a/lisp/ol-docview.el b/lisp/ol-docview.el
index b31f1ce5e..0907ddee1 100644
--- a/lisp/ol-docview.el
+++ b/lisp/ol-docview.el
@@ -83,7 +83,7 @@
       (error "No such file: %s" path))
     (when page (doc-view-goto-page page))))
 
-(defun org-docview-store-link ()
+(defun org-docview-store-link (&optional _interactive?)
   "Store a link to a docview buffer."
   (when (eq major-mode 'doc-view-mode)
     ;; This buffer is in doc-view-mode
diff --git a/lisp/ol-eshell.el b/lisp/ol-eshell.el
index 2c7ec6bef..595dd0ee0 100644
--- a/lisp/ol-eshell.el
+++ b/lisp/ol-eshell.el
@@ -60,7 +60,7 @@ followed by a colon."
     (insert command)
     (eshell-send-input)))
 
-(defun org-eshell-store-link ()
+(defun org-eshell-store-link (&optional _interactive?)
   "Store eshell link.
 When opened, the link switches back to the current eshell buffer and
 the current working directory."
diff --git a/lisp/ol-eww.el b/lisp/ol-eww.el
index 40b820d2b..c13dbf339 100644
--- a/lisp/ol-eww.el
+++ b/lisp/ol-eww.el
@@ -62,7 +62,7 @@
   "Open URL with Eww in the current buffer."
   (eww url))
 
-(defun org-eww-store-link ()
+(defun org-eww-store-link (&optional _interactive?)
   "Store a link to the url of an EWW buffer."
   (when (eq major-mode 'eww-mode)
     (org-link-store-props
diff --git a/lisp/ol-gnus.el b/lisp/ol-gnus.el
index e105fdb2c..b9ee8683f 100644
--- a/lisp/ol-gnus.el
+++ b/lisp/ol-gnus.el
@@ -123,7 +123,7 @@ If `org-store-link' was called with a prefix arg the meaning of
 	      (url-encode-url message-id))
     (concat "gnus:" group "#" message-id)))
 
-(defun org-gnus-store-link ()
+(defun org-gnus-store-link (&optional _interactive?)
   "Store a link to a Gnus folder or message."
   (pcase major-mode
     (`gnus-group-mode
diff --git a/lisp/ol-info.el b/lisp/ol-info.el
index 0edf9a13f..6062cab34 100644
--- a/lisp/ol-info.el
+++ b/lisp/ol-info.el
@@ -50,7 +50,7 @@
                          :insert-description #'org-info-description-as-command)
 
 ;; Implementation
-(defun org-info-store-link ()
+(defun org-info-store-link (&optional _interactive?)
   "Store a link to an Info file and node."
   (when (eq major-mode 'Info-mode)
     (let ((link (concat "info:"
diff --git a/lisp/ol-irc.el b/lisp/ol-irc.el
index 78c4884b0..b263e52db 100644
--- a/lisp/ol-irc.el
+++ b/lisp/ol-irc.el
@@ -103,7 +103,7 @@ attributes that are found."
     parts))
 
 ;;;###autoload
-(defun org-irc-store-link ()
+(defun org-irc-store-link (&optional _interactive?)
   "Dispatch to the appropriate function to store a link to an IRC session."
   (cond
    ((eq major-mode 'erc-mode)
diff --git a/lisp/ol-man.el b/lisp/ol-man.el
index e3f13815e..42aacea81 100644
--- a/lisp/ol-man.el
+++ b/lisp/ol-man.el
@@ -82,7 +82,7 @@ matched strings in man buffer."
             (set-window-point window point)
             (set-window-start window point)))))))
 
-(defun org-man-store-link ()
+(defun org-man-store-link (&optional _interactive?)
   "Store a link to a README file."
   (when (memq major-mode '(Man-mode woman-mode))
     ;; This is a man page, we do make this link
diff --git a/lisp/ol-mhe.el b/lisp/ol-mhe.el
index 106cfedc9..a32481324 100644
--- a/lisp/ol-mhe.el
+++ b/lisp/ol-mhe.el
@@ -80,7 +80,7 @@ supported by MH-E."
 (org-link-set-parameters "mhe" :follow #'org-mhe-open :store #'org-mhe-store-link)
 
 ;; Implementation
-(defun org-mhe-store-link ()
+(defun org-mhe-store-link (&optional _interactive?)
   "Store a link to an MH-E folder or message."
   (when (or (eq major-mode 'mh-folder-mode)
 	    (eq major-mode 'mh-show-mode))
diff --git a/lisp/ol-rmail.el b/lisp/ol-rmail.el
index f6031ab52..f1f753b6f 100644
--- a/lisp/ol-rmail.el
+++ b/lisp/ol-rmail.el
@@ -51,7 +51,7 @@
 			 :store #'org-rmail-store-link)
 
 ;; Implementation
-(defun org-rmail-store-link ()
+(defun org-rmail-store-link (&optional _interactive?)
   "Store a link to an Rmail folder or message."
   (when (or (eq major-mode 'rmail-mode)
 	    (eq major-mode 'rmail-summary-mode))
diff --git a/lisp/ol.el b/lisp/ol.el
index cf59c8556..7e7df468a 100644
--- a/lisp/ol.el
+++ b/lisp/ol.el
@@ -63,7 +63,6 @@
 (declare-function org-find-property "org" (property &optional value))
 (declare-function org-get-heading "org" (&optional no-tags no-todo no-priority no-comment))
 (declare-function org-id-find-id-file "org-id" (id))
-(declare-function org-id-store-link "org-id" ())
 (declare-function org-insert-heading "org" (&optional arg invisible-ok top))
 (declare-function org-load-modules-maybe "org" (&optional force))
 (declare-function org-mark-ring-push "org" (&optional pos buffer))
@@ -620,6 +619,12 @@ If it decides that it is not responsible for this link, it must return
 nil to indicate that Org can continue with other options like
 exact and fuzzy text search.")
 
+(defvar org-link-precise-target-marker (make-marker)
+  "Marker pointing to the target identified for a link search string.
+Each call to `org-link-precise-link-target' will set this marker
+to the location where the returned target was found.  If there
+was no target, the marker will point nowhere.")
+
 
 ;;; Internal Variables
 
@@ -815,6 +820,74 @@ spec."
   (org-with-point-at (car region)
     (not (org-in-regexp org-link-any-re))))
 
+(defun org-link--try-link-store-functions (interactive?)
+  "Try storing external links, prompting if more than one is possible.
+
+Each function returned by `org-store-link-functions' is called in
+turn.  If multiple functions return non-nil, prompt for which
+link should be stored.
+
+Argument INTERACTIVE? indicates whether `org-store-link' was
+called interactively and is passed to the link store functions.
+
+Return t when a link has been stored in `org-link-store-props'."
+  (let ((results-alist nil))
+    (dolist (f (org-store-link-functions))
+      (when (condition-case nil
+                (funcall f interactive?)
+              ;; FIXME: The store function used (< Org 9.7) to accept
+              ;; no arguments; provide backward compatibility support
+              ;; for them.
+              (wrong-number-of-arguments
+               (funcall f)))
+        ;; FIXME: return value is not link's plist, so we store the
+        ;; new value before it is modified.  It would be cleaner to
+        ;; ask store link functions to return the plist instead.
+        (push (cons f (copy-sequence org-store-link-plist))
+              results-alist)))
+    (pcase results-alist
+      (`nil nil)
+      (`((,_ . ,_)) t)	;single choice: nothing to do
+      (`((,name . ,_) . ,_)
+       ;; Reinstate link plist associated to the chosen
+       ;; function.
+       (apply #'org-link-store-props
+              (cdr (assoc-string
+                    (completing-read
+                     (format "Store link with (default %s): " name)
+                     (mapcar #'car results-alist)
+                     nil t nil nil (symbol-name name))
+                    results-alist)))
+       t))))
+
+(defun org-link--add-to-stored-links (link desc)
+  "Add LINK to `org-stored-links' with description DESC."
+  (cond
+   ((not (member (list link desc) org-stored-links))
+    (push (list link desc) org-stored-links)
+    (message "Stored: %s" (or desc link)))
+   ((equal (list link desc) (car org-stored-links))
+    (message "This link has already been stored"))
+   (t
+    (setq org-stored-links
+          (delete (list link desc) org-stored-links))
+    (push (list link desc) org-stored-links)
+    (message "Link moved to front: %s" (or desc link)))))
+
+(defun org-link--file-link-to-here ()
+  "Return as (LINK . DESC) a file link with search string to here."
+  (let ((link (concat "file:"
+                      (abbreviate-file-name
+                       (buffer-file-name (buffer-base-buffer)))))
+        desc)
+    (when org-link-context-for-files
+      (pcase (org-link-precise-link-target)
+        (`nil nil)
+        (`(,search-string . ,search-desc)
+         (setq link (format "%s::%s" link search-string))
+         (setq desc search-desc))))
+    (cons link desc)))
+
 
 ;;; Public API
 
@@ -1041,7 +1114,9 @@ LINK is escaped with backslashes for inclusion in buffer."
   "List of functions that are called to create and store a link.
 
 The functions are defined in the `:store' property of
-`org-link-parameters'.
+`org-link-parameters'.  Each function should accept an argument
+INTERACTIVE? which indicates whether the user has initiated
+`org-store-link' interactively.
 
 Each function will be called in turn until one returns a non-nil
 value.  Each function should check if it is responsible for
@@ -1280,7 +1355,11 @@ respects buffer narrowing."
 	   (yes-or-no-p "No match - create this as a new heading? "))
       (goto-char (point-max))
       (unless (bolp) (newline))
-      (org-insert-heading nil t t)
+      ;; Find appropriate level for new heading
+      (let ((level (save-excursion
+                     (goto-char (point-min))
+                     (+ 1 (or (org-current-level) 0)))))
+        (org-insert-heading nil t level))
       (insert s "\n")
       (forward-line -1))
      ;; Only headlines are looked after.  No need to process
@@ -1332,6 +1411,71 @@ priority cookie or tag."
 	  (org-link--normalize-string
 	   (or string (org-get-heading t t t t)))))
 
+(defun org-link-precise-link-target ()
+  "Determine search string and description for storing a link.
+
+If a search string (see `org-link-search') is found, return cons
+cell (SEARCH-STRING . DESC).  Otherwise, return nil.
+
+If there is an active region, the contents (or a part of it, see
+`org-link-context-for-files') is used as the search string.
+
+In Org buffers, if point is at a named element (such as a source
+block), the name is used for the search string.  If at a heading,
+its CUSTOM_ID is used to form a search string of the form
+\"#id\", if present, otherwise the current heading text is used
+in the form \"*Heading\".
+
+If none of those finds a suitable search string, the current line
+is used as the search string.
+
+The description DESC is nil (meaning the user will be prompted
+for a description when inserting the link) for search strings
+based on a region or the current line.  For other cases, DESC is
+a cleaned-up version of the name or heading at point.
+
+`org-link-precise-target-marker' is set to the location to which the
+search string refers, or to nowhere if a target is not identified."
+  (move-marker org-link-precise-target-marker nil)
+  (let* ((region (org-link--context-from-region))
+         (result
+          (cond
+           (region
+            (move-marker org-link-precise-target-marker (region-beginning))
+            (cons (org-link--normalize-string region t) nil))
+
+           ((derived-mode-p 'org-mode)
+            (let* ((element (org-element-at-point))
+                   (name (org-element-property :name element))
+                   (heading (org-element-lineage element '(headline inlinetask) t))
+                   (custom-id (org-entry-get heading "CUSTOM_ID")))
+              (cond
+               (name
+                (move-marker org-link-precise-target-marker
+                             (org-element-begin element))
+                (cons name name))
+               ((org-before-first-heading-p)
+                (move-marker org-link-precise-target-marker
+                             (line-beginning-position))
+                (cons (org-link--normalize-string (org-current-line-string) t) nil))
+               (heading
+                (move-marker org-link-precise-target-marker
+                             (org-element-begin heading))
+                (cons (if custom-id (concat "#" custom-id)
+                        (org-link-heading-search-string))
+                      (org-link--normalize-string
+                       (org-get-heading t t t t)))))))
+
+           ;; Not in an org-mode buffer, no region
+           (t
+            (move-marker org-link-precise-target-marker
+                         (line-beginning-position))
+            (cons (org-link--normalize-string (org-current-line-string) t) nil)))))
+
+    ;; Only use search option if there is some text.
+    (when (org-string-nw-p (car result))
+      result)))
+
 (defun org-link-open-as-file (path in-emacs)
   "Pretend PATH is a file name and open it.
 
@@ -1404,7 +1548,7 @@ PATH is a symbol name, as a string."
     ((and (pred boundp) variable) (describe-variable variable))
     (name (user-error "Unknown function or variable: %s" name))))
 
-(defun org-link--store-help ()
+(defun org-link--store-help (&optional _interactive?)
   "Store \"help\" type link."
   (when (eq major-mode 'help-mode)
     (let ((symbol
@@ -1539,7 +1683,12 @@ prefix ARG forces storing a link for each line in the
 active region.
 
 Assume the function is called interactively if INTERACTIVE? is
-non-nil."
+non-nil.
+
+In Org buffers, an additional \"human-readable\" simple file link
+is stored as an alternative to persistent org-id or other links,
+if at a heading with a CUSTOM_ID property or an element with a
+NAME."
   (interactive "P\np")
   (org-load-modules-maybe)
   (if (and (equal arg '(64)) (org-region-active-p))
@@ -1554,36 +1703,19 @@ non-nil."
 	    (move-beginning-of-line 2)
 	    (set-mark (point)))))
     (setq org-store-link-plist nil)
-    (let (link cpltxt desc search custom-id agenda-link) ;; description
+    ;; Negate `org-context-in-file-links' when given a single universal arg.
+    (let ((org-link-context-for-files (org-xor org-link-context-for-files
+                                               (equal arg '(4))))
+          link cpltxt desc search agenda-link) ;; description
       (cond
        ;; Store a link using an external link type, if any function is
-       ;; available. If more than one can generate a link from current
-       ;; location, ask which one to use.
+       ;; available, unless external link types are skipped for this
+       ;; call using two universal args.  If more than one function
+       ;; can generate a link from current location, ask the user
+       ;; which one to use.
        ((and (not (equal arg '(16)))
-	     (let ((results-alist nil))
-	       (dolist (f (org-store-link-functions))
-		 (when (funcall f)
-		   ;; XXX: return value is not link's plist, so we
-		   ;; store the new value before it is modified.  It
-		   ;; would be cleaner to ask store link functions to
-		   ;; return the plist instead.
-		   (push (cons f (copy-sequence org-store-link-plist))
-			 results-alist)))
-	       (pcase results-alist
-		 (`nil nil)
-		 (`((,_ . ,_)) t)	;single choice: nothing to do
-		 (`((,name . ,_) . ,_)
-		  ;; Reinstate link plist associated to the chosen
-		  ;; function.
-		  (apply #'org-link-store-props
-			 (cdr (assoc-string
-			       (completing-read
-                                (format "Store link with (default %s): " name)
-                                (mapcar #'car results-alist)
-                                nil t nil nil (symbol-name name))
-			       results-alist)))
-		  t))))
-	(setq link (plist-get org-store-link-plist :link))
+             (org-link--try-link-store-functions interactive?))
+        (setq link (plist-get org-store-link-plist :link))
         ;; If store function actually set `:description' property, use
         ;; it, even if it is nil.  Otherwise, fallback to nil (ask user).
 	(setq desc (plist-get org-store-link-plist :description)))
@@ -1634,6 +1766,7 @@ non-nil."
 	    (org-with-point-at m
 	      (setq agenda-link (org-store-link nil interactive?))))))
 
+       ;; Calendar mode
        ((eq major-mode 'calendar-mode)
 	(let ((cd (calendar-cursor-to-date)))
 	  (setq link
@@ -1642,6 +1775,7 @@ non-nil."
 		 (org-encode-time 0 0 0 (nth 1 cd) (nth 0 cd) (nth 2 cd))))
 	  (org-link-store-props :type "calendar" :date cd)))
 
+       ;; Image mode
        ((eq major-mode 'image-mode)
 	(setq cpltxt (concat "file:"
 			     (abbreviate-file-name buffer-file-name))
@@ -1659,15 +1793,22 @@ non-nil."
 	  (setq cpltxt (concat "file:" file)
 		link cpltxt)))
 
+       ;; Try `org-create-file-search-functions`.  If any are
+       ;; successful, create a file link to the current buffer with
+       ;; the provided search string.  (sets `link` and `cpltxt` to
+       ;; the same thing; it looks like the intention originally was
+       ;; that cpltxt was a description, which might have been set by
+       ;; the search-function (removed in switch to lexical binding)).
        ((setq search (run-hook-with-args-until-success
 		      'org-create-file-search-functions))
 	(setq link (concat "file:" (abbreviate-file-name buffer-file-name)
 			   "::" search))
 	(setq cpltxt (or link))) ;; description
 
+       ;; Main logic for storing built-in link types in org-mode
+       ;; buffers
        ((and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode))
 	(org-with-limited-levels
-	 (setq custom-id (org-entry-get nil "CUSTOM_ID"))
 	 (cond
 	  ;; Store a link using the target at point
 	  ((org-in-regexp "[^<]<<\\([^<>]+\\)>>[^>]" 1)
@@ -1681,74 +1822,21 @@ non-nil."
                  ;; links.  Maybe the case of identical target and
                  ;; description should be handled by `org-insert-link'.
                  cpltxt nil
-                 desc nil
-                 ;; Do not append #CUSTOM_ID link below.
-                 custom-id nil))
-	  ((and (featurep 'org-id)
-		(or (eq org-id-link-to-org-use-id t)
-		    (and interactive?
-			 (or (eq org-id-link-to-org-use-id 'create-if-interactive)
-			     (and (eq org-id-link-to-org-use-id
-				      'create-if-interactive-and-no-custom-id)
-				  (not custom-id))))
-		    (and org-id-link-to-org-use-id (org-entry-get nil "ID"))))
-	   ;; Store a link using the ID at point
-	   (setq link (condition-case nil
-			  (prog1 (org-id-store-link)
-			    (setq desc (plist-get org-store-link-plist :description)))
-			(error
-			 ;; Probably before first headline, link only to file
-			 (concat "file:"
-				 (abbreviate-file-name
-				  (buffer-file-name (buffer-base-buffer))))))))
-	  (t
+                 desc nil))
+          (t
 	   ;; Just link to current headline.
-	   (setq cpltxt (concat "file:"
-				(abbreviate-file-name
-				 (buffer-file-name (buffer-base-buffer)))))
-	   ;; Add a context search string.
-	   (when (org-xor org-link-context-for-files (equal arg '(4)))
-	     (let* ((element (org-element-at-point))
-		    (name (org-element-property :name element))
-		    (context
-		     (cond
-		      ((let ((region (org-link--context-from-region)))
-			 (and region (org-link--normalize-string region t))))
-		      (name)
-		      ((org-before-first-heading-p)
-		       (org-link--normalize-string (org-current-line-string) t))
-		      (t (org-link-heading-search-string)))))
-	       (when (org-string-nw-p context)
-		 (setq cpltxt (format "%s::%s" cpltxt context))
-		 (setq desc
-		       (or name
-			   ;; Although description is not a search
-			   ;; string, use `org-link--normalize-string'
-			   ;; to prettify it (contiguous white spaces)
-			   ;; and remove volatile contents (statistics
-			   ;; cookies).
-			   (and (not (org-before-first-heading-p))
-				(org-link--normalize-string
-				 (org-get-heading t t t t)))
-			   "NONE")))))
-	   (setq link cpltxt)))))
+           (let ((here (org-link--file-link-to-here)))
+             (setq cpltxt (car here))
+             (setq desc (cdr here)))
+           (setq link cpltxt)))))
 
+       ;; Buffer linked to file, but not an org-mode buffer.
        ((buffer-file-name (buffer-base-buffer))
 	;; Just link to this file here.
-	(setq cpltxt (concat "file:"
-			     (abbreviate-file-name
-			      (buffer-file-name (buffer-base-buffer)))))
-	;; Add a context search string.
-	(when (org-xor org-link-context-for-files (equal arg '(4)))
-	  (let ((context (org-link--normalize-string
-			  (or (org-link--context-from-region)
-			      (org-current-line-string))
-			  t)))
-	    ;; Only use search option if there is some text.
-	    (when (org-string-nw-p context)
-	      (setq cpltxt (format "%s::%s" cpltxt context))
-	      (setq desc "NONE"))))
-	(setq link cpltxt))
+        (let ((here (org-link--file-link-to-here)))
+          (setq cpltxt (car here))
+          (setq desc (cdr here)))
+        (setq link cpltxt))
 
        (interactive?
 	(user-error "No method for storing a link from this buffer"))
@@ -1764,24 +1852,18 @@ non-nil."
       ;; Store and return the link
       (if (not (and interactive? link))
 	  (or agenda-link (and link (org-link-make-string link desc)))
-        (dotimes (_ (if custom-id 2 1)) ; Store 2 links when CUSTOM-ID is non-nil.
-          (cond
-           ((not (member (list link desc) org-stored-links))
-            (push (list link desc) org-stored-links)
-	    (message "Stored: %s" (or desc link)))
-           ((equal (list link desc) (car org-stored-links))
-            (message "This link has already been stored"))
-           (t
-            (setq org-stored-links
-                  (delete (list link desc) org-stored-links))
-            (push (list link desc) org-stored-links)
-            (message "Link moved to front: %s" (or desc link))))
-	  (when custom-id
-	    (setq link (concat "file:"
-			       (abbreviate-file-name
-			        (buffer-file-name (buffer-base-buffer)))
-			       "::#" custom-id))))
-	(car org-stored-links)))))
+        (org-link--add-to-stored-links link desc)
+        ;; In org buffers, store an additional "human-readable" link
+        ;; using custom id, if available.
+        (when (and (buffer-file-name (buffer-base-buffer))
+                   (derived-mode-p 'org-mode)
+                   (org-entry-get nil "CUSTOM_ID"))
+          (let ((here (org-link--file-link-to-here)))
+            (setq link (car here))
+            (setq desc (cdr here)))
+          (unless (equal (list link desc) (car org-stored-links))
+            (org-link--add-to-stored-links link desc)))
+        (car org-stored-links)))))
 
 ;;;###autoload
 (defun org-insert-link (&optional complete-file link-location description)
diff --git a/lisp/org-id.el b/lisp/org-id.el
index 8647a57cc..7200be34d 100644
--- a/lisp/org-id.el
+++ b/lisp/org-id.el
@@ -129,6 +129,46 @@ nil   Never use an ID to make a link, instead link using a text search for
 	  (const :tag "Only use existing" use-existing)
 	  (const :tag "Do not use ID to create link" nil)))
 
+(defcustom org-id-link-consider-parent-id nil
+  "Non-nil means storing a link to an Org entry considers inherited IDs.
+
+When this option is non-nil and `org-id-link-use-context' is
+enabled, ID properties inherited from parent entries will be
+considered when storing an ID link.  If no ID is found in this
+way, a new one may be created as normal (see
+`org-id-link-to-org-use-id').
+
+For example, given this org file:
+
+* Parent
+:PROPERTIES:
+:ID: abc
+:END:
+** Child 1
+** Child 2
+
+With `org-id-link-consider-parent-id' and
+`org-id-link-use-context' both enabled, storing a link with point
+at \"Child 1\" will produce a link \"<id:abc::*Child 1>\".  This
+allows linking to uniquely-named sub-entries within a parent
+entry with an ID, without requiring every sub-entry to have its
+own ID."
+  :group 'org-link-store
+  :group 'org-id
+  :package-version '(Org . "9.7")
+  :type 'boolean)
+
+(defcustom org-id-link-use-context t
+  "Non-nil means enables search string context in org-id links.
+
+Search strings are added by `org-id-store-link' when both the
+general option `org-link-context-for-files' and the org-id option
+`org-id-link-use-context' are non-nil."
+  :group 'org-link-store
+  :group 'org-id
+  :package-version '(Org . "9.7")
+  :type 'boolean)
+
 (defcustom org-id-uuid-program "uuidgen"
   "The uuidgen program."
   :group 'org-id
@@ -280,15 +320,21 @@ This is useful when working with contents in a temporary buffer
 that will be copied back to the original.")
 
 ;;;###autoload
-(defun org-id-get (&optional epom create prefix)
-  "Get the ID property of the entry at EPOM.
-EPOM is an element, marker, or buffer position.
-If EPOM is nil, refer to the entry at point.
-If the entry does not have an ID, the function returns nil.
-However, when CREATE is non-nil, create an ID if none is present already.
-PREFIX will be passed through to `org-id-new'.
-In any case, the ID of the entry is returned."
-  (let ((id (org-entry-get epom "ID")))
+(defun org-id-get (&optional epom create prefix inherit)
+  "Get the ID of the entry at EPOM.
+
+EPOM is an element, marker, or buffer position.  If EPOM is nil,
+refer to the entry at point.
+
+If INHERIT is non-nil, ID properties inherited from parent
+entries are considered.  Otherwise, only ID properties on the
+entry itself are considered.
+
+When CREATE is nil, return the ID of the entry if found,
+otherwise nil.  When CREATE is non-nil, create an ID if none has
+been found, and return the new ID.  PREFIX will be passed through
+to `org-id-new'."
+  (let ((id (org-entry-get epom "ID" (and inherit t))))
     (cond
      ((and id (stringp id) (string-match "\\S-" id))
       id)
@@ -703,18 +749,56 @@ optional argument MARKERP, return the position as a new marker."
 ;; Calling the following function is hard-coded into `org-store-link',
 ;; so we do have to add it to `org-store-link-functions'.
 
+(defun org-id--get-id-to-store-link (&optional create)
+  "Get or create the relevant ID for storing a link.
+
+Optional argument CREATE is passed to `org-id-get'.
+
+Inherited IDs are only considered when
+`org-id-link-consider-parent-id', `org-id-link-use-context' and
+`org-link-context-for-files' are all enabled, since inherited IDs
+are confusing without the additional search string context.
+
+Note that this function resets the
+`org-entry-property-inherited-from' marker: it will either point
+to nil (if the id was not inherited) or to the point it was
+inherited from."
+  (let* ((inherit-id (and org-id-link-consider-parent-id
+                          org-id-link-use-context
+                          org-link-context-for-files)))
+    (move-marker org-entry-property-inherited-from nil)
+    (org-id-get nil create nil inherit-id)))
+
 ;;;###autoload
 (defun org-id-store-link ()
   "Store a link to the current entry, using its ID.
 
-If before first heading store first title-keyword as description
-or filename if no title."
+The link description is based on the heading, or if before the
+first heading, the title keyword if available, or else the
+filename.
+
+When `org-link-context-for-files' and `org-id-link-use-context'
+are non-nil, add a search string to the link.  The link
+description is then based on the search string target.
+
+When in addition `org-id-link-consider-parent-id' is non-nil, the
+ID can be inherited from a parent entry, with the search string
+used to still link to the current location."
   (interactive)
-  (when (and (buffer-file-name (buffer-base-buffer)) (derived-mode-p 'org-mode))
-    (let* ((link (concat "id:" (org-id-get-create)))
+  (when (and (buffer-file-name (buffer-base-buffer))
+             (derived-mode-p 'org-mode))
+    ;; Get the precise target first, in case looking for an id causes
+    ;; a properties drawer to be added at the current location.
+    (let* ((precise-target (and org-link-context-for-files
+                                org-id-link-use-context
+                                (org-link-precise-link-target)))
+           (link (concat "id:" (org-id--get-id-to-store-link 'create)))
+           (id-location (or (and org-entry-property-inherited-from
+                                 (marker-position org-entry-property-inherited-from))
+                            (save-excursion (org-back-to-heading-or-point-min t) (point))))
 	   (case-fold-search nil)
 	   (desc (save-excursion
-		   (org-back-to-heading-or-point-min t)
+                   (goto-char id-location)
                    (cond ((org-before-first-heading-p)
                           (let ((keywords (org-collect-keywords '("TITLE"))))
                             (if keywords
@@ -726,14 +810,61 @@ or filename if no title."
 			      (match-string 4)
 			    (match-string 0)))
                          (t link)))))
+      ;; Precise targets should be after id-location to avoid
+      ;; duplicating the current headline as a search string
+      (when (and precise-target
+                 org-link-precise-target-marker
+                 (> (marker-position org-link-precise-target-marker)
+                    id-location))
+         (setq link (concat link "::" (car precise-target)))
+         (setq desc (cdr precise-target)))
       (org-link-store-props :link link :description desc :type "id")
       link)))
 
-(defun org-id-open (id _)
-  "Go to the entry with id ID."
-  (org-mark-ring-push)
-  (let ((m (org-id-find id 'marker))
-	cmd)
+;;;###autoload
+(defun org-id-store-link-maybe (&optional interactive?)
+  "Store a link to the current entry using its ID if enabled.
+
+The value of `org-id-link-to-org-use-id' determines whether an ID
+link should be stored, using `org-id-store-link'.
+
+Assume the function is called interactively if INTERACTIVE? is
+non-nil."
+  (when (and (buffer-file-name (buffer-base-buffer))
+             (derived-mode-p 'org-mode)
+             (or (eq org-id-link-to-org-use-id t)
+                 (and interactive?
+                      (or (eq org-id-link-to-org-use-id 'create-if-interactive)
+                          (and (eq org-id-link-to-org-use-id
+                                   'create-if-interactive-and-no-custom-id)
+                               (not (org-entry-get nil "CUSTOM_ID")))))
+                 ;; 'use-existing
+                 (and org-id-link-to-org-use-id
+                      (org-id--get-id-to-store-link))))
+    (org-id-store-link)))
+
+(defun org-id-open (link _)
+  "Go to the entry indicated by id link LINK.
+
+The link can include a search string after \"::\", which is
+passed to `org-link-search'.
+
+For backwards compatibility with IDs that contain \"::\", if no
+match is found for the ID, the full link string including \"::\"
+will be tried as an ID."
+  (let* ((option (and (string-match "::\\(.*\\)\\'" link)
+		      (match-string 1 link)))
+	 (id (if (not option) link
+               (substring link 0 (match-beginning 0))))
+         m cmd)
+    (org-mark-ring-push)
+    (setq m (org-id-find id 'marker))
+    (when (and (not m) option)
+      ;; Backwards compatibility: if id is not found, try treating
+      ;; whole link as an id.
+      (setq m (org-id-find link 'marker))
+      (when m
+        (setq option nil)))
     (unless m
       (error "Cannot find entry with ID \"%s\"" id))
     ;; Use a buffer-switching command in analogy to finding files
@@ -750,9 +881,16 @@ or filename if no title."
 	(funcall cmd (marker-buffer m)))
     (goto-char m)
     (move-marker m nil)
+    (when option
+      (save-restriction
+        (unless (org-before-first-heading-p)
+          (org-narrow-to-subtree))
+        (org-link-search option)))
     (org-fold-show-context)))
 
-(org-link-set-parameters "id" :follow #'org-id-open)
+(org-link-set-parameters "id"
+  :follow #'org-id-open
+  :store #'org-id-store-link-maybe)
 
 (provide 'org-id)
 
diff --git a/lisp/org-lint.el b/lisp/org-lint.el
index 4d2a55d15..b23afcca3 100644
--- a/lisp/org-lint.el
+++ b/lisp/org-lint.el
@@ -65,6 +65,7 @@
 ;; - special properties in properties drawers,
 ;; - obsolete syntax for properties drawers,
 ;; - invalid duration in EFFORT property,
+;; - invalid ID property with a double colon,
 ;; - missing definition for footnote references,
 ;; - missing reference for footnote definitions,
 ;; - non-footnote definitions in footnote section,
@@ -686,6 +687,16 @@ Use :header-args: instead"
 	       (list (org-element-begin p)
 		     (format "Invalid effort duration format: %S" value))))))))
 
+(defun org-lint-invalid-id-property (ast)
+  (org-element-map ast 'node-property
+    (lambda (p)
+      (when (equal "ID" (org-element-property :key p))
+	(let ((value (org-element-property :value p)))
+	  (and (org-string-nw-p value)
+               (string-match-p "::" value)
+	       (list (org-element-begin p)
+		     (format "IDs should not include \"::\": %S" value))))))))
+
 (defun org-lint-link-to-local-file (ast)
   (org-element-map ast 'link
     (lambda (l)
@@ -1684,6 +1695,11 @@ AST is the buffer parse tree."
   #'org-lint-invalid-effort-property
   :categories '(properties))
 
+(org-lint-add-checker 'invalid-id-property
+  "Report search string delimiter \"::\" in ID property"
+  #'org-lint-invalid-id-property
+  :categories '(properties))
+
 (org-lint-add-checker 'undefined-footnote-reference
   "Report missing definition for footnote references"
   #'org-lint-undefined-footnote-reference
diff --git a/testing/lisp/test-ol.el b/testing/lisp/test-ol.el
index e0cec0854..4be6b3055 100644
--- a/testing/lisp/test-ol.el
+++ b/testing/lisp/test-ol.el
@@ -381,6 +381,136 @@ See https://github.com/yantar92/org/issues/4."
 	 (equal (format "[[file:%s::*foo bar][foo bar]]" file file)
 		(org-store-link nil)))))))
 
+(ert-deftest test-org-link/precise-link-target ()
+  "Test `org-link-precise-link-target` specifications."
+  (org-test-with-temp-text "* H1<point>\n* H2\n"
+    (should
+     (equal '("*H1" . "H1")
+            (org-link-precise-link-target)))
+    (should
+     (equal 1 (marker-position org-link-precise-target-marker))))
+  (org-test-with-temp-text "* H1\n#+name: foo<point>\n#+begin_example\nhi\n#+end_example\n"
+    (should
+     (equal '("foo" . "foo")
+            (org-link-precise-link-target)))
+    (should
+     (equal 6 (marker-position org-link-precise-target-marker))))
+  (org-test-with-temp-text "\nText<point>\n* H1\n"
+    (should
+     (equal '("Text" . nil)
+            (org-link-precise-link-target)))
+    (should
+     (equal 2 (marker-position org-link-precise-target-marker))))
+  (org-test-with-temp-text "\n<point>\n* H1\n"
+    (should
+     (equal nil (org-link-precise-link-target)))
+    (should
+     (equal 2 (marker-position org-link-precise-target-marker)))))
+
+(defmacro test-ol-stored-link-with-text (text &rest body)
+  "Return :link and :description from link stored in body."
+  (declare (indent 1))
+  `(let (org-store-link-plist)
+     (org-test-with-temp-text-in-file ,text
+       ,@body
+       (list (plist-get org-store-link-plist :link)
+             (plist-get org-store-link-plist :description)))))
+
+(ert-deftest test-org-link/id-store-link ()
+  "Test `org-id-store-link' specifications."
+  (let ((org-id-link-to-org-use-id nil))
+    (should
+     (equal '(nil nil)
+            (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n"
+              (org-id-store-link-maybe t)))))
+  ;; On a headline, link to that headline's ID.  Use heading as the
+  ;; description of the link.
+  (let ((org-id-link-to-org-use-id t))
+    (should
+     (equal '("id:abc" "H1")
+            (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n"
+              (org-id-store-link-maybe t)))))
+  ;; Remove TODO keywords etc from description of the link.
+  (let ((org-id-link-to-org-use-id t))
+    (should
+     (equal '("id:abc" "H1")
+            (test-ol-stored-link-with-text "* TODO [#A] H1 :tag:\n:PROPERTIES:\n:ID: abc\n:END:\n"
+              (org-id-store-link-maybe t)))))
+  ;; create-if-interactive
+  (let ((org-id-link-to-org-use-id 'create-if-interactive))
+    (should
+     (equal '("id:abc" "H1")
+            (cl-letf (((symbol-function 'org-id-new)
+                       (lambda (&rest _rest) "abc")))
+              (test-ol-stored-link-with-text "* H1\n"
+                (org-id-store-link-maybe t)))))
+    (should
+     (equal '(nil nil)
+            (test-ol-stored-link-with-text "* H1\n"
+              (org-id-store-link-maybe nil)))))
+  ;; create-if-interactive-and-no-custom-id
+  (let ((org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id))
+    (should
+     (equal '("id:abc" "H1")
+            (cl-letf (((symbol-function 'org-id-new)
+                       (lambda (&rest _rest) "abc")))
+              (test-ol-stored-link-with-text "* H1\n"
+                (org-id-store-link-maybe t)))))
+    (should
+     (equal '(nil nil)
+            (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:CUSTOM_ID: xyz\n:END:\n"
+              (org-id-store-link-maybe t))))
+    (should
+     (equal '(nil nil)
+            (test-ol-stored-link-with-text "* H1\n"
+              (org-id-store-link-maybe nil)))))
+  ;; use-context should have no effect when on the headline with an id
+  (let ((org-id-link-to-org-use-id t)
+        (org-id-link-use-context t))
+    (should
+     (equal '("id:abc" "H2")
+            (test-ol-stored-link-with-text "* H1\n** H2<point>\n:PROPERTIES:\n:ID: abc\n:END:\n"
+              ;; simulate previously getting an inherited value
+              (move-marker org-entry-property-inherited-from 1)
+              (org-id-store-link-maybe t))))))
+
+(ert-deftest test-org-link/id-store-link-using-parent ()
+  "Test `org-id-store-link' specifications with `org-id-link-consider-parent-id` set."
+  ;; when using context to still find specific heading
+  (let ((org-id-link-to-org-use-id t)
+        (org-id-link-consider-parent-id t)
+        (org-id-link-use-context t))
+    (should
+     (equal '("id:abc::*H2" "H2")
+            (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n<point>"
+              (org-id-store-link))))
+    (should
+     (equal '("id:abc::name" "name")
+            (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n\n#+name: name\n<point>#+begin_example\nhi\n#+end_example\n"
+              (org-id-store-link))))
+    (should
+     (equal '("id:abc" "H1")
+            (test-ol-stored-link-with-text "* H1<point>\n:PROPERTIES:\n:ID: abc\n:END:\n** H2\n"
+              (org-id-store-link))))
+    ;; should not use newly added ids as search string, e.g. in an empty file
+    (should
+     (let (name result)
+       (setq result
+             (cl-letf (((symbol-function 'org-id-new)
+                        (lambda (&rest _rest) "abc")))
+               (test-ol-stored-link-with-text "<point>"
+                 (setq name (buffer-name))
+                 (org-id-store-link))))
+       (equal `("id:abc" ,name) result))))
+  ;; should not find targets in the next section
+  (let ((org-id-link-to-org-use-id 'use-existing)
+        (org-id-link-consider-parent-id t)
+        (org-id-link-use-context t))
+    (should
+     (equal '(nil nil)
+            (test-ol-stored-link-with-text "* H1\n:PROPERTIES:\n:ID: abc\n:END:\n* H2\n** <point>Target\n"
+              (org-id-store-link-maybe t))))))
+
 
 ;;; Radio Targets
 
-- 
2.37.1 (Apple Git-137.1)

